├── .gitignore ├── .travis.yml ├── CI ├── ci └── codecov ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── README_CN.md ├── Sources ├── WWDCHelper │ └── main.swift ├── WWDCHelperKit │ ├── Extensions.swift │ ├── Network.swift │ ├── SessionInfoParsable.swift │ ├── WWDCHelper.swift │ ├── WWDCParser.swift │ └── WWDCSession.swift └── WWDCWebVTTToSRTHelperKit │ ├── Extensions.swift │ ├── WWDCWebVTTToSRTHelper.swift │ └── WebVTTParsable.swift ├── Tests ├── Fixtures │ ├── SampleContent.html │ ├── fileSequence0.webvtt │ ├── fileSequence1.webvtt │ └── fileSequence2.webvtt ├── WWDCHelperKitTests │ └── WWDCHelperKitTests.swift └── WWDCWebVTTToSRTHelperKitTests │ └── WWDCWebVTTToSRTHelperKitTests.swift ├── WWDCHelper-h.png ├── codecov.yml ├── install.sh └── resources ├── 202_hd_advances_in_tvmlkit.chs.srt ├── Design.sketch └── logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - osx 3 | language: generic 4 | sudo: required 5 | dist: trusty 6 | osx_image: xcode10.3 7 | script: 8 | - eval "$(curl -sL https://raw.githubusercontent.com/kingcos/WWDCHelper/master/CI/ci)" 9 | - eval "$(curl -sL https://raw.githubusercontent.com/kingcos/WWDCHelper/master/CI/codecov)" 10 | -------------------------------------------------------------------------------- /CI/ci: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION="5.0" 4 | echo "Swift $VERSION Continuous Integration"; 5 | 6 | # Determine OS 7 | UNAME=`uname`; 8 | if [[ $UNAME == "Darwin" ]]; 9 | then 10 | OS="macos"; 11 | else 12 | if [[ $UNAME == "Linux" ]]; 13 | then 14 | UBUNTU_RELEASE=`lsb_release -a 2>/dev/null`; 15 | if [[ $UBUNTU_RELEASE == *"16.04"* ]]; 16 | then 17 | OS="ubuntu1604"; 18 | else 19 | OS="ubuntu1404"; 20 | fi 21 | else 22 | echo "Unsupported Operating System: $UNAME"; 23 | fi 24 | fi 25 | echo "🖥 Operating System: $OS"; 26 | 27 | if [[ $OS != "macos" ]]; 28 | then 29 | echo "📚 Installing Dependencies" 30 | sudo apt-get install -y clang libicu-dev uuid-dev 31 | 32 | echo "🐦 Installing Swift"; 33 | if [[ $OS == "ubuntu1604" ]]; 34 | then 35 | SWIFTFILE="swift-$VERSION-RELEASE-ubuntu16.04"; 36 | else 37 | SWIFTFILE="swift-$VERSION-RELEASE-ubuntu14.04"; 38 | fi 39 | wget https://swift.org/builds/swift-$VERSION-release/$OS/swift-$VERSION-RELEASE/$SWIFTFILE.tar.gz 40 | tar -zxf $SWIFTFILE.tar.gz 41 | export PATH=$PWD/$SWIFTFILE/usr/bin:"${PATH}" 42 | fi 43 | 44 | echo "📅 Version: `swift --version`"; 45 | 46 | echo "🚀 Building"; 47 | swift build 48 | if [[ $? != 0 ]]; 49 | then 50 | echo "❌ Build failed"; 51 | exit 1; 52 | fi 53 | 54 | echo "💼 Building Release"; 55 | swift build -c release 56 | if [[ $? != 0 ]]; 57 | then 58 | echo "❌ Build for release failed"; 59 | exit 1; 60 | fi 61 | 62 | echo "🔎 Testing"; 63 | 64 | swift test 65 | if [[ $? != 0 ]]; 66 | then 67 | echo "❌ Tests failed"; 68 | exit 1; 69 | fi 70 | 71 | echo "✅ Done" 72 | -------------------------------------------------------------------------------- /CI/codecov: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION="5.0" 4 | echo "Swift $VERSION CodeCov Integration"; 5 | 6 | # Determine OS 7 | UNAME=`uname`; 8 | if [[ $UNAME == "Darwin" ]]; 9 | then 10 | OS="macos"; 11 | else 12 | echo "🚫 Unsupported OS: $UNAME, skipping..."; 13 | exit 0; 14 | fi 15 | echo "🖥 Operating System: $OS"; 16 | 17 | 18 | PROJ_OUTPUT=`swift package generate-xcodeproj`; 19 | PROJ_NAME="${PROJ_OUTPUT/generated: .\//}" 20 | SCHEME_NAME="${PROJ_NAME/.xcodeproj/}" 21 | 22 | echo "🚀 Testing: $SCHEME_NAME"; 23 | 24 | rvm install 2.3.7 25 | gem install xcpretty 26 | WORKING_DIRECTORY=$(PWD) xcodebuild -project $PROJ_NAME -scheme $SCHEME_NAME -sdk macosx10.14 -destination arch=x86_64 -configuration Debug -enableCodeCoverage YES test | xcpretty 27 | bash <(curl -s https://codecov.io/bash) 28 | 29 | echo "✅ Done"; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 萌面大道 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 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CommandLineKit", 6 | "repositoryURL": "https://github.com/kingcos/CommandLine.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "2d6cd9c49c2f4d3e16ccf1d9795b04903ea3567d", 10 | "version": "4.2.0" 11 | } 12 | }, 13 | { 14 | "package": "PathKit", 15 | "repositoryURL": "https://github.com/kylef/PathKit.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511", 19 | "version": "1.0.0" 20 | } 21 | }, 22 | { 23 | "package": "Rainbow", 24 | "repositoryURL": "https://github.com/onevcat/Rainbow.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "9c52c1952e9b2305d4507cf473392ac2d7c9b155", 28 | "version": "3.1.5" 29 | } 30 | }, 31 | { 32 | "package": "Spectre", 33 | "repositoryURL": "https://github.com/kylef/Spectre.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", 37 | "version": "0.9.0" 38 | } 39 | } 40 | ] 41 | }, 42 | "version": 1 43 | } 44 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "WWDCHelper", 8 | dependencies: [ 9 | .package(url: "https://github.com/kylef/PathKit.git", from: "1.0.0"), 10 | .package(url: "https://github.com/onevcat/Rainbow.git", from: "3.1.5"), 11 | .package(url: "https://github.com/kylef/Spectre.git", from: "0.9.0"), 12 | .package(url: "https://github.com/kingcos/CommandLine.git", from: "4.2.0") 13 | ], 14 | targets: [ 15 | .target( 16 | name: "WWDCHelper", 17 | dependencies: ["WWDCHelperKit", "CommandLine", "Rainbow"]), 18 | .target( 19 | name: "WWDCHelperKit", 20 | dependencies: ["WWDCWebVTTToSRTHelperKit", "PathKit", "Rainbow"]), 21 | .target( 22 | name: "WWDCWebVTTToSRTHelperKit", 23 | dependencies: []), 24 | .testTarget( 25 | name: "WWDCHelperKitTests", 26 | dependencies: ["WWDCHelperKit", "PathKit", "Spectre"]), 27 | .testTarget( 28 | name: "WWDCWebVTTToSRTHelperKitTests", 29 | dependencies: ["WWDCWebVTTToSRTHelperKit", "PathKit", "Spectre"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | WWDCHelper Logo 3 |

4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 | # WWDCHelper 14 | 15 | English | [中文](README_CN.md) 16 | 17 | > Inspired by qiaoxueshi/WWDC_2015_Video_Subtitle, ohoachuck/wwdc-downloader, and @onevcat's videos. Thanks for their inspiration and efforts. 👏 18 | 19 | ## Info 20 | 21 | WWDCHelper is a command line tool on macOS for you to get WWDC info easily. Now you can get download links of SD/HD video & PDF, and download subtitles in English, Janpanese (only WWDC 2018 & 2019), and even Simplified Chinese directly by it. 22 | 23 | You can also download subtitles at the [releases](https://github.com/kingcos/WWDCHelper/releases) page. 24 | 25 | > **Notice:** 26 | > 27 | > Although I have written in Swift for years, I still have a lot to learn about Swift. And to be honest, CLI (Command Line Interface) is not familiar for me. So this program is not perfect, even a little wired. So you can issue me if you have any questions, advices or find some bugs . I will be very appreciated for your help. ❤️ 28 | 29 | ## How 30 | 31 | ### Install 32 | 33 | You should have [Swift Package Manager](https://swift.org/package-manager/) installed or latest Xcode installed with command line tools in your macOS. 34 | 35 | ```sh 36 | > git clone https://github.com/kingcos/WWDCHelper.git 37 | > cd WWDCHelper 38 | > ./install.sh 39 | ``` 40 | 41 | ### Run 42 | 43 | ![WWDCHelper -h](WWDCHelper-h.png) 44 | 45 | ### Demo 46 | 47 | - *Update*: If you want to get all sessions info of WWDC 2019 (Including videos' download links): 48 | 49 | ```sh 50 | > wwdchelper -y 2019 51 | ``` 52 | 53 | - *Update*: - If you want to download subtitles in English of WWDC 2019: 54 | 55 | ```sh 56 | # HD Videos: 57 | > wwdchelper -y 2019 -l eng 58 | or 59 | # SD Videos: 60 | > wwdchelper -y 2019 --sd -l eng 61 | ``` 62 | 63 | - If you just want to get Session 202 & 203 info of WWDC 2019: 64 | 65 | ```sh 66 | > wwdchelper -s 202 203 67 | or 68 | > wwdchelper -y 2019 -s 202 203 69 | or 70 | > wwdchelper --year 2019 --sesions 202 203 71 | ``` 72 | 73 | - If you want to download subtitles in English of Session 202 & 203 for SD videos: 74 | 75 | ```sh 76 | > wwdchelper -s 202 203 -l eng --sd 77 | or 78 | > wwdchelper --year 2019 --sessions 202 203 --language eng --sd 79 | ``` 80 | 81 | - If you want to download **all** subtitles in English for HD videos, and specify the path (**NOT recommend**): 82 | 83 | ```sh 84 | > wwdchelper -l eng -p /Users/kingcos/Downloads/hd/eng/ 85 | ``` 86 | 87 | ### NOT Implemented 88 | 89 | > Maybe implement these features in the future. 90 | 91 | - [x] ~~Download multiple subtitles at once~~ 92 | - [x] ~~Support subtitles in all languages that provided~~ 93 | - [x] ~~Support ALL WWDC (2012 ~ 2019)~~ 94 | - [x] ~~Swift 4.1~~ 95 | - [x] ~~Swift 4.2~~ 96 | - [x] ~~Swift 5.0~~ 97 | - [ ] Support for Linux 🐧 98 | 99 | ### Reference 100 | 101 | - [qiaoxueshi/WWDC_2015_Video_Subtitle](https://github.com/qiaoxueshi/WWDC_2015_Video_Subtitle) 102 | - [ohoachuck/wwdc-downloader](https://github.com/ohoachuck/wwdc-downloader) 103 | - [onevcat](https://github.com/onevcat) 104 | - [onevcat/FengNiao](https://github.com/onevcat/FengNiao) 105 | 106 | ## LICENSE 107 | 108 | - MIT 109 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 |

2 | WWDCHelper Logo 3 |

4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 | # WWDCHelper 14 | 15 | [English](README.md) | 中文 16 | 17 | > 受 qiaoxueshi/WWDC_2015_Video_Subtitle,ohoachuck/wwdc-downloader,以及 @onevcat 的视频启发。感谢他们的灵感与努力。👏 18 | 19 | [English Version README](README.md) 20 | 21 | ## 简介 22 | 23 | WWDCHelper 是一个 macOS 命令行工具,以便于获取 WWDC 官方的资源。现在,你可以用它直接获取 SD/HD 视频和对应 PDF 文档的链接,也可以直接下载英文、日文(仅限 WWDC 2018 & 2019)、甚至**简体中文**的字幕。 24 | 25 | 当然,你也可以直接在 [releases](https://github.com/kingcos/WWDCHelper/releases) 页面仅下载字幕。 26 | 27 | > **提示** 28 | > 29 | > 虽然确实写了几年 Swift,但仍有不足,仍有差距。加上对命令行程序的不太了解,可能该项目并非很好,甚至有点怪异。如果您找到了问题、或是建议、又或是 Bug,都欢迎您提出 Issue。我会非常感谢您的帮助。❤️ 30 | 31 | ## 如何使用 32 | 33 | ### 安装 34 | 35 | 您的 macOS 需要安装了 [Swift Package Manager](https://swift.org/package-manager/),或者安装了最新版本的 Xcode 并带有命令行工具。 36 | 37 | ```sh 38 | > git clone https://github.com/kingcos/WWDCHelper.git 39 | > cd WWDCHelper 40 | > ./install.sh 41 | ``` 42 | 43 | ### 运行 44 | 45 | ![WWDCHelper -h](WWDCHelper-h.png) 46 | 47 | ### Demo 48 | 49 | - *Update*: 如果您需要获取 WWDC 2019 所有 Session 信息(包括视频的下载链接): 50 | 51 | ```sh 52 | > wwdchelper -y 2019 53 | ``` 54 | 55 | - *Update*: 如果您需要下载 WWDC 2019 所有英文字幕(**官网最新简体中文字幕已更新至 Releases 页面**): 56 | 57 | ```sh 58 | # HD 视频: 59 | > wwdchelper -y 2019 -l eng 60 | or 61 | # SD 视频: 62 | > wwdchelper -y 2019 --sd -l eng 63 | ``` 64 | 65 | - 如果您仅需要 WWDC 2019 中 Session 202 和 203 的信息: 66 | 67 | ```sh 68 | > wwdchelper -s 202 203 69 | or 70 | > wwdchelper -y 2019 -s 202 203 71 | or 72 | > wwdchelper --year 2019 --sesions 202 203 73 | ``` 74 | 75 | - 如果您想要为 Session 202 和 203 的 SD(清晰度)视频下载简体中文字幕: 76 | 77 | ```sh 78 | > wwdchelper -s 202 203 -l chs --sd 79 | or 80 | > wwdchelper --year 2019 --sessions 202 203 --language chs --sd 81 | ``` 82 | 83 | - 如果您想要为**所有** Session 的 HD(清晰度)视频下载简体中文字幕,并指定路径(**不推荐**): 84 | 85 | ```sh 86 | > wwdchelper -l chs -p /Users/kingcos/Downloads/hd/chs/ 87 | ``` 88 | 89 | ### 未实现 90 | 91 | > 可能会在未来实现以下特点: 92 | 93 | - [x] ~~一次性下载多个字幕~~ 94 | - [x] ~~支持所有官网提供字幕~~ 95 | - [x] ~~支持所有年份 WWDC(2012~2019)~~ 96 | - [x] ~~Swift 4.1 支持~~ 97 | - [x] ~~Swift 4.2 支持~~ 98 | - [x] ~~Swift 5.0 支持~~ 99 | - [ ] 支持 Linux 🐧 100 | 101 | ### 参考 102 | 103 | - [qiaoxueshi/WWDC_2015_Video_Subtitle](https://github.com/qiaoxueshi/WWDC_2015_Video_Subtitle) 104 | - [ohoachuck/wwdc-downloader](https://github.com/ohoachuck/wwdc-downloader) 105 | - [onevcat](https://github.com/onevcat) 106 | - [onevcat/FengNiao](https://github.com/onevcat/FengNiao) 107 | 108 | ## 许可 109 | 110 | - MIT 111 | -------------------------------------------------------------------------------- /Sources/WWDCHelper/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // WWDCHelper 4 | // 5 | // Created by kingcos on 07/09/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CommandLineKit 11 | import Rainbow 12 | import WWDCHelperKit 13 | 14 | let appVersion = "v1.1.0" 15 | let cli = CommandLineKit.CommandLine() 16 | 17 | cli.formatOutput = { s, type in 18 | var str: String 19 | switch(type) { 20 | case .error: 21 | str = s.red.bold 22 | case .optionFlag: 23 | str = s.green.underline 24 | case .optionHelp: 25 | str = s.blue 26 | default: 27 | str = s 28 | } 29 | 30 | return cli.defaultFormat(s: str, type: type) 31 | } 32 | 33 | let yearOption = StringOption(shortFlag: "y", longFlag: "year", 34 | helpMessage: "Setup the year of WWDC. Support ALL WWDCs from `2012` to `2019` now! Default is WWDC 2019.") 35 | let sessionIDsOption = MultiStringOption(shortFlag: "s", longFlag: "sessions", 36 | helpMessage: "Setup the session numbers in WWDC. Default is all sessions.") 37 | let subtitleLanguageOption = StringOption(shortFlag: "l", longFlag: "language", 38 | helpMessage: "Setup the language of subtitle. Support `chs`, `eng`, and `jpn` (only WWDC 2018 & 2019) now! Default is Simplified Chinese.") 39 | let isSubtitleForSDVideoOption = BoolOption(longFlag: "sd", 40 | helpMessage: "Add sd tag for subtitle\'s filename. Default is for hd videos.") 41 | let subtitlePathOption = StringOption(shortFlag: "p", longFlag: "path", 42 | helpMessage: "Setup the download path of subtitles. Default is current folder.") 43 | let helpOption = BoolOption(shortFlag: "h", longFlag: "help", 44 | helpMessage: "Print the help info.") 45 | let versionOption = BoolOption(shortFlag: "v", longFlag: "version", 46 | helpMessage: "Print the version info.") 47 | 48 | cli.addOptions(yearOption, 49 | sessionIDsOption, 50 | subtitleLanguageOption, 51 | isSubtitleForSDVideoOption, 52 | subtitlePathOption, 53 | helpOption, 54 | versionOption) 55 | 56 | do { 57 | try cli.parse() 58 | } catch { 59 | cli.printUsage(error) 60 | exit(EX_USAGE) 61 | } 62 | 63 | if helpOption.value { 64 | cli.printUsage() 65 | exit(EX_OK) 66 | } 67 | 68 | if versionOption.value { 69 | print(appVersion) 70 | exit(EX_OK); 71 | } 72 | 73 | let year = yearOption.value 74 | let sessionIDs = sessionIDsOption.value 75 | let subtitleLanguage: String? = subtitleLanguageOption.value?.lowercased() 76 | let subtitlePath = subtitlePathOption.value 77 | let isSubtitleForSDVideo = isSubtitleForSDVideoOption.value 78 | 79 | var helper = WWDCHelper(year: year, 80 | sessionIDs: sessionIDs, 81 | subtitleLanguage: subtitleLanguage, 82 | subtitlePath: subtitlePath, 83 | isSubtitleForSDVideo: isSubtitleForSDVideo) 84 | 85 | do { 86 | print("Welcome to WWDCHelper by github.com/kingcos! 👏") 87 | print("Please wait a little while.\nHelper is trying to fetch your favorite WWDC info hard...") 88 | try helper.enterHelper() 89 | } catch { 90 | print("If you have any issues, please contact with me at github.com/kingcos.") 91 | guard let err = error as? HelperError else { 92 | print("Unknown Error: \(error)".red.bold) 93 | exit(EX_USAGE) 94 | } 95 | 96 | switch err { 97 | case .unknownYear: 98 | print("\(year!) hasn't been supported currently. Now support WWDC 2012 ~ WWDC 2019 same as developer official website.".red.bold) 99 | case .unknownSubtitleLanguage: 100 | print("Language \(subtitleLanguage!) is NOT supported for now, WWDC support Simpliefied Chinese, Japanese (for WWDC 2018 & 2019) and English.".red.bold) 101 | case .unknownSessionID: 102 | print("Session ID was not found, please check it.".red.bold) 103 | case .subtitlePathNotExist: 104 | print("The path does NOT exist, please check it.") 105 | } 106 | 107 | exit(EX_USAGE) 108 | } 109 | -------------------------------------------------------------------------------- /Sources/WWDCHelperKit/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // WWDCHelperKit 4 | // 5 | // Created by kingcos on 07/09/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | var wholeNSRange: NSRange { 13 | return NSRange(location: 0, length: count) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/WWDCHelperKit/Network.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Network.swift 3 | // WWDCHelperKit 4 | // 5 | // Created by kingcos on 08/09/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | struct Network { 12 | 13 | static let shared = Network() 14 | 15 | func fetchContent(of url: String) -> String { 16 | let request = URLRequest(url: URL(string: url)!) 17 | let session = URLSession.shared 18 | let semaphore = DispatchSemaphore(value: 0) 19 | 20 | var result = "" 21 | let task = session.dataTask(with: request) { data, response, error in 22 | guard let data = data, error == nil else { 23 | print(error!.localizedDescription) 24 | return 25 | } 26 | result = String(data: data, encoding: .utf8) ?? "" 27 | semaphore.signal() 28 | } 29 | 30 | task.resume() 31 | semaphore.wait() 32 | return result 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/WWDCHelperKit/SessionInfoParsable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionInfoParsable.swift 3 | // WWDCHelperKit 4 | // 5 | // Created by kingcos on 07/09/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | enum SessionInfoType { 12 | case subtitleIndexURLPrefix 13 | case resources 14 | case sessionsInfo 15 | } 16 | 17 | protocol SessionInfoParsable { 18 | func parseSubtitleIndexURLPrefix(in content: String) -> String 19 | func parseResourceURLs(in content: String) -> [String] 20 | func parseSessionsInfo(in content: String) -> [String : String] 21 | } 22 | 23 | protocol RegexSessionInfoParsable: SessionInfoParsable { 24 | var patterns: [SessionInfoType : String] { get } 25 | } 26 | 27 | extension RegexSessionInfoParsable { 28 | func parseSubtitleIndexURLPrefix(in content: String) -> String { 29 | let nsStr = NSString(string: content) 30 | let regex = try! NSRegularExpression(pattern: patterns[.subtitleIndexURLPrefix]!) 31 | let range = content.wholeNSRange 32 | let matches = regex.matches(in: content, range: range) 33 | 34 | var result = "" 35 | for match in matches { 36 | let firstRange = match.range(at: 1) 37 | result = nsStr.substring(with: firstRange) 38 | } 39 | 40 | return result 41 | } 42 | 43 | func parseResourceURLs(in content: String) -> [String] { 44 | let nsStr = NSString(string: content) 45 | let regex = try! NSRegularExpression(pattern: patterns[.resources]!) 46 | let range = content.wholeNSRange 47 | let matches = regex.matches(in: content, range: range) 48 | 49 | var result = [String]() 50 | for match in matches { 51 | let firstRange = match.range(at: 1) 52 | let secondRange = match.range(at: 3) 53 | let thirdRange = match.range(at: 5) 54 | 55 | result.append(nsStr.substring(with: firstRange)) 56 | result.append(nsStr.substring(with: secondRange)) 57 | result.append(nsStr.substring(with: thirdRange)) 58 | } 59 | 60 | return result 61 | } 62 | 63 | func parseSessionsInfo(in content: String) -> [String : String] { 64 | let nsStr = NSString(string: content) 65 | let regex = try! NSRegularExpression(pattern: patterns[.sessionsInfo]!) 66 | let range = content.wholeNSRange 67 | let matches = regex.matches(in: content, range: range) 68 | 69 | var result = [String : String]() 70 | for match in matches { 71 | let firstRange = match.range(at: 1) 72 | let secondRange = match.range(at: 2) 73 | result[nsStr.substring(with: firstRange)] = nsStr.substring(with: secondRange) 74 | } 75 | 76 | return result 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/WWDCHelperKit/WWDCHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDCHelper.swift 3 | // WWDCHelperKit 4 | // 5 | // Created by kingcos on 06/09/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | import PathKit 11 | import Rainbow 12 | import WWDCWebVTTToSRTHelperKit 13 | 14 | public enum WWDCYear: String { 15 | case wwdc2019 = "wwdc2019" 16 | case wwdc2018 = "wwdc2018" 17 | case wwdc2017 = "wwdc2017" 18 | case wwdc2016 = "wwdc2016" 19 | case wwdc2015 = "wwdc2015" 20 | case wwdc2014 = "wwdc2014" 21 | case wwdc2013 = "wwdc2013" 22 | case wwdc2012 = "wwdc2012" 23 | case unknown 24 | 25 | init(_ value: String?) { 26 | guard let value = value else { 27 | self = .wwdc2019 28 | return 29 | } 30 | 31 | switch value.lowercased() { 32 | case "wwdc2019", "2019": 33 | self = .wwdc2019 34 | case "wwdc2018", "2018": 35 | self = .wwdc2018 36 | case "wwdc2017", "2017": 37 | self = .wwdc2017 38 | case "wwdc2016", "2016": 39 | self = .wwdc2016 40 | case "wwdc2015", "2015": 41 | self = .wwdc2015 42 | case "wwdc2014", "2014": 43 | self = .wwdc2014 44 | case "wwdc2013", "2013": 45 | self = .wwdc2013 46 | case "wwdc2012", "2012": 47 | self = .wwdc2012 48 | default: 49 | self = .unknown 50 | } 51 | } 52 | } 53 | 54 | public enum SubtitleLanguage: String { 55 | case eng = "eng" 56 | case chs = "zho" 57 | case jpn = "jpn" 58 | case empty 59 | case unknown 60 | 61 | init(_ value: String?) { 62 | guard let value = value else { 63 | self = .empty 64 | return 65 | } 66 | 67 | switch value { 68 | case "eng": 69 | self = .eng 70 | case "chs": 71 | self = .chs 72 | case "jpn": 73 | self = .jpn 74 | default: 75 | self = .unknown 76 | } 77 | } 78 | } 79 | 80 | public enum HelperError: Error { 81 | case unknownYear 82 | case unknownSubtitleLanguage 83 | case unknownSessionID 84 | case subtitlePathNotExist 85 | } 86 | 87 | public struct WWDCHelper { 88 | public let year: WWDCYear 89 | public let sessionIDs: [String]? 90 | 91 | public let subtitleLanguage: SubtitleLanguage 92 | public let subtitlePath: Path 93 | public let isSubtitleForSDVideo: Bool 94 | 95 | let srtHelper = WWDCWebVTTToSRTHelper() 96 | var sessionsInfo = [String : String]() 97 | 98 | public init(year: String? = nil, 99 | sessionIDs: [String]? = nil, 100 | subtitleLanguage: String? = nil, 101 | subtitlePath: String? = nil, 102 | isSubtitleForSDVideo: Bool = false) { 103 | self.year = WWDCYear(year) 104 | self.sessionIDs = sessionIDs 105 | self.subtitleLanguage = SubtitleLanguage(subtitleLanguage) 106 | self.subtitlePath = Path(subtitlePath ?? ".").absolute() 107 | self.isSubtitleForSDVideo = isSubtitleForSDVideo 108 | } 109 | } 110 | 111 | extension WWDCHelper { 112 | public mutating func enterHelper() throws { 113 | guard year != .unknown else { throw HelperError.unknownYear } 114 | guard subtitleLanguage != .unknown else { throw HelperError.unknownSubtitleLanguage } 115 | 116 | let sessions = try getSessions(by: sessionIDs, 117 | with: WWDCParser.shared).sorted { $0.id < $1.id } 118 | 119 | if subtitleLanguage != .empty { 120 | if !subtitlePath.exists { 121 | throw HelperError.subtitlePathNotExist 122 | } else { 123 | try downloadData(sessions, with: WWDCParser.shared) 124 | } 125 | } else { 126 | _ = sessions.map { $0.output(year) } 127 | } 128 | } 129 | 130 | func downloadData(_ sessions: [WWDCSession], with parser: RegexSessionInfoParsable) throws { 131 | print("Start downloading...") 132 | 133 | for session in sessions { 134 | var filename = "\(session.id)" 135 | if isSubtitleForSDVideo { 136 | filename += "_sd_" 137 | } else { 138 | filename += "_hd_" 139 | } 140 | 141 | filename += session.title.lowercased() 142 | .replacingOccurrences(of: " ", with: "_") 143 | .replacingOccurrences(of: "/", with: "") 144 | filename = filename + "." + subtitleLanguage.rawValue + ".srt" 145 | 146 | let path = subtitlePath + filename 147 | 148 | guard !FileManager.default.fileExists(atPath: path.string) else { 149 | print("\(filename) already exists, skip to download.") 150 | continue 151 | } 152 | 153 | guard let urls = getWebVTTURLs(with: getResourceURLs(by: session.id, with: parser), and: parser) 154 | else { continue } 155 | 156 | let content = urls 157 | .map { url -> [String] in 158 | let content = Network.shared.fetchContent(of: url) 159 | if content.contains("WEBVTT") { 160 | return content.components(separatedBy: "\n") 161 | } else { 162 | return [] 163 | } 164 | } 165 | let strArr = content.flatMap { $0.map { $0 } } 166 | 167 | if strArr.isEmpty { 168 | // Apple maybe upload empty content... 169 | print("\(filename) downloaded error.".red.bold) 170 | } else { 171 | guard let result = srtHelper.parse(strArr), 172 | let data = result.data(using: .utf8) else { return } 173 | 174 | print(filename, "is downloading...") 175 | 176 | try data.write(to: path.url) 177 | } 178 | } 179 | print("Download successfully.".green.bold) 180 | } 181 | } 182 | 183 | extension WWDCHelper { 184 | mutating func getSessions(by ids: [String]? = nil, with parser: RegexSessionInfoParsable) throws -> [WWDCSession] { 185 | if sessionsInfo.isEmpty { 186 | sessionsInfo = getSessionsInfo(with: parser) 187 | } 188 | let sessionIDs = ids ?? sessionsInfo.map { $0.0 } 189 | 190 | var sessions = [WWDCSession]() 191 | for sessionID in sessionIDs { 192 | guard let session = try getSession(by: sessionID, with: parser) else { continue } 193 | sessions.append(session) 194 | } 195 | 196 | return sessions 197 | } 198 | 199 | mutating func getSession(by id: String, with parser: RegexSessionInfoParsable) throws -> WWDCSession? { 200 | if sessionsInfo.isEmpty { 201 | sessionsInfo = getSessionsInfo(with: parser) 202 | } 203 | guard let title = sessionsInfo[id] else { throw HelperError.unknownSessionID } 204 | let resources = getResourceURLs(by: id, with: parser) 205 | let url = getSubtitleIndexURL(with: resources, and: parser) 206 | 207 | return WWDCSession(id, title, resources, url) 208 | } 209 | } 210 | 211 | extension WWDCHelper { 212 | func getSessionsInfo(with parser: RegexSessionInfoParsable) -> [String : String] { 213 | let url = "https://developer.apple.com/videos/\(year.rawValue)/" 214 | let content = Network.shared.fetchContent(of: url) 215 | return parser.parseSessionsInfo(in: content) 216 | } 217 | 218 | func getResourceURLs(by id: String, with parser: RegexSessionInfoParsable) -> [String] { 219 | let url = "https://developer.apple.com/videos/play/\(year.rawValue)/\(id)/" 220 | let content = Network.shared.fetchContent(of: url) 221 | return parser.parseResourceURLs(in: content) 222 | } 223 | 224 | func getSubtitleIndexURLPrefix(with resources: [String], and parser: RegexSessionInfoParsable) -> String? { 225 | if resources.isEmpty { 226 | return nil 227 | } 228 | return parser.parseSubtitleIndexURLPrefix(in: resources[0]) 229 | } 230 | 231 | func getSubtitleIndexURL(with resources: [String], and parser: RegexSessionInfoParsable) -> String? { 232 | guard let prefix = getSubtitleIndexURLPrefix(with: resources, and: parser) else { return nil } 233 | return prefix + "/subtitles/eng/prog_index.m3u8" 234 | } 235 | 236 | func getWebVTTURLs(with resources: [String], and parser: RegexSessionInfoParsable) -> [String]? { 237 | guard let urlPrefix = getSubtitleIndexURLPrefix(with: resources, and: parser), 238 | let url = getSubtitleIndexURL(with: resources, and: parser) else { return nil } 239 | let content = Network.shared.fetchContent(of: url) 240 | var filesCount = content 241 | .components(separatedBy: "\n") 242 | .filter { $0.hasPrefix("fileSequence") } 243 | .count 244 | 245 | if filesCount == 0 { 246 | filesCount = 100 247 | } 248 | var result = [String]() 249 | for i in 0 ..< filesCount { 250 | result.append(urlPrefix + "/subtitles/\(subtitleLanguage.rawValue)/fileSequence\(i).webvtt") 251 | } 252 | return result 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /Sources/WWDCHelperKit/WWDCParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDCParser.swift 3 | // WWDCHelper 4 | // 5 | // Created by kingcos on 2018/8/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public class WWDCParser: RegexSessionInfoParsable { 11 | static let shared = WWDCParser() 12 | 13 | private init() {} 14 | 15 | let patterns: [SessionInfoType : String] = [ 16 | .subtitleIndexURLPrefix: "(http.*)\\/.*_hd", 17 | .resources: "[\\s\\S]*[\\s\\S]*[\\s\\S]*", 18 | .sessionsInfo: "(.*)" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /Sources/WWDCHelperKit/WWDCSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDCSession.swift 3 | // WWDCHelperKit 4 | // 5 | // Created by kingcos on 06/09/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Rainbow 11 | 12 | public enum WWDCSessionResourceType: String { 13 | case hdVideo = "HD Video" 14 | case sdVideo = "SD Video" 15 | case pdf = "PDF" 16 | } 17 | 18 | public struct WWDCSession: Equatable { 19 | public let id: String 20 | public let title: String 21 | public let subtitleIndexURL: String? 22 | public var resources: [WWDCSessionResourceType : String] 23 | 24 | init(_ id: String, 25 | _ title: String, 26 | _ resources: [String], 27 | _ subtitleIndexURL: String? = nil) { 28 | self.id = id 29 | self.title = title 30 | self.resources = [WWDCSessionResourceType : String]() 31 | 32 | var resources = resources 33 | while resources.count < 3 { 34 | resources.append("") 35 | } 36 | 37 | self.resources[.hdVideo] = resources[0] 38 | self.resources[.sdVideo] = resources[1] 39 | self.resources[.pdf] = resources[2] 40 | self.subtitleIndexURL = subtitleIndexURL 41 | } 42 | 43 | func output(_ year: WWDCYear) { 44 | print(year.rawValue.uppercased().bold, "- Session \(id) - \(title)".bold) 45 | if let hdVideo = resources[.hdVideo], hdVideo != "" { 46 | print("\(WWDCSessionResourceType.hdVideo.rawValue) Download:", "\n\(hdVideo)".underline) 47 | } 48 | 49 | if let sdVideo = resources[.sdVideo], sdVideo != "" { 50 | print("\(WWDCSessionResourceType.sdVideo.rawValue) Download:", "\n\(sdVideo)".underline) 51 | } 52 | 53 | if let pdf = resources[.pdf], pdf != "" { 54 | print("\(WWDCSessionResourceType.pdf.rawValue) Download:", "\n\(pdf)".underline) 55 | } 56 | print("- - - - - - - - - -".red) 57 | } 58 | } 59 | 60 | 61 | public func ==(lhs: WWDCSession, rhs: WWDCSession) -> Bool { 62 | let sdVideoFlag = lhs.resources[.sdVideo] != rhs.resources[.sdVideo] 63 | let hdVideoFlag = lhs.resources[.hdVideo] != rhs.resources[.hdVideo] 64 | let pdfFlag = lhs.resources[.pdf] != rhs.resources[.pdf] 65 | 66 | if lhs.id != rhs.id 67 | || lhs.title != rhs.title 68 | || sdVideoFlag 69 | || hdVideoFlag 70 | || pdfFlag 71 | || lhs.subtitleIndexURL != rhs.subtitleIndexURL { 72 | return false 73 | } 74 | 75 | return true 76 | } 77 | 78 | -------------------------------------------------------------------------------- /Sources/WWDCWebVTTToSRTHelperKit/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // WWDCWevVTTToSRTHelperKit 4 | // 5 | // Created by kingcos on 09/09/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | var wholeNSRange: NSRange { 13 | return NSRange(location: 0, length: count) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/WWDCWebVTTToSRTHelperKit/WWDCWebVTTToSRTHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDCWebVTTToSRTHelper.swift 3 | // WWDCWevVTTToSRTHelperKit 4 | // 5 | // Created by kingcos on 09/09/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public struct WWDCWebVTTToSRTHelper { 12 | 13 | let parser = WWDCWebVTTParser() 14 | 15 | public init() { 16 | } 17 | 18 | public func parse(_ strArr: [String]) -> String? { 19 | return parser.parseToSRT(strArr) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/WWDCWebVTTToSRTHelperKit/WebVTTParsable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebVTTParsable.swift 3 | // WWDCWevVTTToSRTHelperKit 4 | // 5 | // Created by kingcos on 09/09/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | enum WebVTTParseType { 12 | case timeline 13 | case subtitle 14 | } 15 | 16 | protocol WebVTTParsable { 17 | func parseToSRT(_ strArr: [String]) -> String? 18 | } 19 | 20 | protocol RegexWebVTTParsable: WebVTTParsable { 21 | var patterns: [WebVTTParseType : String] { get } 22 | 23 | func removeHeader(_ contentArr: inout [String]) 24 | func removeBlankLines(_ contentArr: inout [String]) 25 | func addLineNumbers(_ contentArr: inout [String]) 26 | func dealWithLines(_ contentArr: inout [String]) 27 | func replaceCharacters(_ contentArr: inout [String]) 28 | } 29 | 30 | extension RegexWebVTTParsable { 31 | func parseToSRT(_ strArr: [String]) -> String? { 32 | var contentArr = strArr.map { $0.components(separatedBy: "\n") } 33 | .flatMap { $0.map { $0 } } 34 | removeHeader(&contentArr) 35 | removeBlankLines(&contentArr) 36 | addLineNumbers(&contentArr) 37 | dealWithLines(&contentArr) 38 | replaceCharacters(&contentArr) 39 | return contentArr 40 | .reduce("") { $0 + "\n" + $1 } 41 | } 42 | 43 | func removeHeader(_ contentArr: inout [String]) { 44 | contentArr = contentArr.map { $0 45 | .components(separatedBy: "\n") 46 | .filter { !($0.hasPrefix("WEBVTT") || $0.hasPrefix("X-TIMESTAMP-MAP")) } 47 | .reduce("") { $0 + $1 } 48 | } 49 | } 50 | 51 | func removeBlankLines(_ contentArr: inout [String]) { 52 | contentArr = contentArr.filter { $0 != "" } 53 | } 54 | 55 | func addLineNumbers(_ contentArr: inout [String]) { 56 | var n = 1 57 | var i = 0 58 | while i < contentArr.count { 59 | if contentArr[i].contains("-->") { 60 | contentArr.insert("\(n)", at: i) 61 | contentArr.insert("", at: i) 62 | n += 1 63 | i += 2 64 | } 65 | i += 1 66 | } 67 | contentArr.removeFirst() 68 | } 69 | 70 | func dealWithLines(_ contentArr: inout [String]) { 71 | var result = [String]() 72 | for content in contentArr { 73 | let nsStr = NSString(string: content) 74 | let range = content.wholeNSRange 75 | let timelineRegex = try! NSRegularExpression(pattern: patterns[.timeline]!) 76 | let subtitleRegex = try! NSRegularExpression(pattern: patterns[.subtitle]!) 77 | let timelineMatches = timelineRegex.matches(in: content, range: range) 78 | let subtitleMatches = subtitleRegex.matches(in: content, range: range) 79 | 80 | if !timelineMatches.isEmpty { 81 | for match in timelineMatches { 82 | let firstRange = match.range(at: 1) 83 | result.append(nsStr.substring(with: firstRange).replacingOccurrences(of: ".", with: ",")) 84 | } 85 | } else if !subtitleMatches.isEmpty { 86 | for match in subtitleMatches { 87 | let firstRange = match.range(at: 1) 88 | result.append(nsStr.substring(with: firstRange)) 89 | } 90 | } else { 91 | result.append(content) 92 | } 93 | } 94 | 95 | contentArr = result 96 | } 97 | 98 | func replaceCharacters(_ contentArr: inout [String]) { 99 | contentArr = contentArr.map { 100 | $0.replacingOccurrences(of: ">", with: ">") 101 | .replacingOccurrences(of: "<", with: "<") 102 | .replacingOccurrences(of: "&", with: "&") 103 | } 104 | } 105 | } 106 | 107 | public struct WWDCWebVTTParser: RegexWebVTTParsable { 108 | var patterns: [WebVTTParseType : String] = [ 109 | .timeline: "(.*-->.*[0-9]{3})", 110 | .subtitle: "<.*>(.*)" 111 | ] 112 | } 113 | -------------------------------------------------------------------------------- /Tests/Fixtures/SampleContent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample HTML 5 | 6 | 7 | <

A sample HTML for WWDCHelper tests.

8 | 9 | -------------------------------------------------------------------------------- /Tests/Fixtures/fileSequence0.webvtt: -------------------------------------------------------------------------------- 1 | WEBVTT 2 | X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000 3 | 4 | 00:00:07.516 --> 00:00:16.500 A:middle 5 | [ 背景音 ] 6 | 7 | 00:00:21.516 --> 00:00:27.806 A:middle 8 | [ 掌声 ] 9 | 10 | 00:00:28.306 --> 00:00:29.576 A:middle 11 | >> 嗨 大家好 12 | 13 | 00:00:29.666 --> 00:00:31.456 A:middle 14 | 我叫 Carolyn Cranfill 15 | 16 | 00:00:31.456 --> 00:00:32.735 A:middle 17 | 是 Apple 人机界面组的 18 | 19 | 00:00:32.735 --> 00:00:34.726 A:middle 20 | 一名设计师 21 | 22 | 00:00:34.926 --> 00:00:37.086 A:middle 23 | 也领导了包容性设计的 24 | 25 | 00:00:37.086 --> 00:00:38.116 A:middle 26 | 工作 27 | 28 | 00:00:38.916 --> 00:00:40.976 A:middle 29 | 谢谢你们今天的到来 30 | 31 | 00:00:42.086 --> 00:00:43.596 A:middle 32 | 上个月 33 | 34 | 00:00:43.596 --> 00:00:45.666 A:middle 35 | 我们陆续发布了七个视频 36 | 37 | 00:00:46.106 --> 00:00:47.726 A:middle 38 | 着重刻画了那些因为做着自己喜欢的事情 39 | 40 | 00:00:47.726 --> 00:00:48.396 A:middle 41 | 而充满能量的人们 42 | 43 | 00:00:48.766 --> 00:00:49.816 A:middle 44 | 现在我想和你们分享 45 | 46 | 00:00:49.816 --> 00:00:51.136 A:middle 47 | 其中一个 48 | 49 | 00:00:53.166 --> 00:00:56.066 A:middle 50 | Carols:刚开始我只是想 51 | 52 | 00:00:56.196 --> 00:00:57.116 A:middle 53 | 证明人们错了 54 | 55 | 00:00:57.316 --> 00:00:59.056 A:middle 56 | VoiceOver:辅助功能 57 | 58 | 00:00:59.516 --> 00:01:00.446 A:middle 59 | 视觉 Voiceover 60 | 61 | -------------------------------------------------------------------------------- /Tests/Fixtures/fileSequence1.webvtt: -------------------------------------------------------------------------------- 1 | WEBVTT 2 | X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000 3 | 4 | 00:00:59.516 --> 00:01:00.446 A:middle 5 | 视觉 Voiceover 6 | 7 | 00:01:00.526 --> 00:01:01.936 A:middle 8 | 开启 9 | 10 | 00:01:02.056 --> 00:01:02.446 A:middle 11 | 计划到达 12 | 13 | 00:01:02.446 --> 00:01:03.000 A:middle 14 | 第二个地点 15 | 16 | 00:01:03.546 --> 00:01:05.495 A:middle 17 | Carols:我们不希望我们的第一张专辑 18 | 19 | 00:01:05.495 --> 00:01:06.576 A:middle 20 | 听上去像本地乐队 21 | 22 | 00:01:06.966 --> 00:01:08.206 A:middle 23 | VoiceOver:预计 10 分钟到达 24 | 25 | 00:01:08.206 --> 00:01:09.506 A:middle 26 | Carols:我们希望我们的第一张专辑 27 | 28 | 00:01:09.506 --> 00:01:10.566 A:middle 29 | 听上去更像一个专业乐队 30 | 31 | 00:01:10.566 --> 00:01:11.826 A:middle 32 | 我们已经为此努力了很多年 33 | 34 | 00:01:13.376 --> 00:01:14.056 A:middle 35 | VoiceOver:发送搭车请求 36 | 37 | 00:01:14.056 --> 00:01:14.906 A:middle 38 | 正在联系附近司机 39 | 40 | 00:01:14.986 --> 00:01:15.596 A:middle 41 | 找到可用车 42 | 43 | 00:01:17.266 --> 00:01:17.626 A:middle 44 | [ 鼓声 ] 45 | 46 | 00:01:17.686 --> 00:01:19.896 A:middle 47 | Carols:我们叫 Distartica 48 | 49 | 00:01:19.896 --> 00:01:20.096 A:middle 50 | 我们演奏重金属音乐 51 | 52 | 00:01:21.516 --> 00:01:24.326 A:middle 53 | [ 音乐 ] 54 | 55 | 00:01:24.826 --> 00:01:26.336 A:middle 56 | Siri:左转进入 57 | 58 | 00:01:26.806 --> 00:01:26.926 A:middle 59 | Empanada 车道 60 | 61 | 00:01:27.316 --> 00:01:27.556 A:middle 62 | Man:[ 笑 ] Empanada(肉馅卷饼) 63 | 64 | 00:01:27.616 --> 00:01:28.166 A:middle 65 | Carols:Empanada 车道 66 | 67 | 00:01:28.166 --> 00:01:28.926 A:middle 68 | Man:我听着都饿了[ 笑 ] 69 | 70 | 00:01:29.476 --> 00:01:30.836 A:middle 71 | Carols:我最终成为了乐队的 72 | 73 | 00:01:30.966 --> 00:01:32.176 A:middle 74 | 公关经理 75 | 76 | 00:01:32.176 --> 00:01:33.016 A:middle 77 | 我以前不知道这个工作是 78 | 79 | 00:01:33.156 --> 00:01:33.366 A:middle 80 | 做什么的 81 | 82 | 00:01:33.406 --> 00:01:35.176 A:middle 83 | 然后我就是这样的 84 | 85 | 00:01:35.646 --> 00:01:36.366 A:middle 86 | 为什么要打标签 87 | 88 | 00:01:36.396 --> 00:01:36.676 A:middle 89 | 要做什么 90 | 91 | 00:01:36.766 --> 00:01:37.536 A:middle 92 | #金属 93 | 94 | 00:01:37.536 --> 00:01:38.606 A:middle 95 | #新音乐 96 | 97 | 00:01:38.606 --> 00:01:40.276 A:middle 98 | 或者在这种情况下 99 | 100 | 00:01:40.276 --> 00:01:42.000 A:middle 101 | 要写#debut album 之类的 102 | 103 | 00:01:48.326 --> 00:01:49.446 A:middle 104 | VoiceOver:信息 105 | 106 | 00:01:49.686 --> 00:01:50.446 A:middle 107 | 来自 ReverbNation 108 | 109 | 00:01:51.456 --> 00:01:52.326 A:middle 110 | 双击以打开 111 | 112 | 00:01:52.826 --> 00:01:53.376 A:middle 113 | 文本框 114 | 115 | 00:01:53.686 --> 00:01:54.216 A:middle 116 | 听写 117 | 118 | 00:01:54.936 --> 00:01:57.036 A:middle 119 | Carols:专辑将在 2017 年 4 月 14 日 120 | 121 | 00:01:57.036 --> 00:02:00.576 A:middle 122 | 全球发行 感叹号 123 | 124 | -------------------------------------------------------------------------------- /Tests/Fixtures/fileSequence2.webvtt: -------------------------------------------------------------------------------- 1 | WEBVTT 2 | X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000 3 | 4 | 00:01:57.036 --> 00:02:00.576 A:middle 5 | 全球发行 感叹号 6 | 7 | 00:02:01.666 --> 00:02:04.516 A:middle 8 | 关注我们的 ReverbNation 页面 句号 9 | 10 | 00:02:05.466 --> 00:02:07.786 A:middle 11 | [ 点按 ]VoiceOver:完成 已成功分享 12 | 13 | 00:02:08.515 --> 00:02:20.500 A:middle 14 | [ 音乐 ] 15 | 16 | 00:02:21.516 --> 00:02:27.946 A:middle 17 | [ 掌声 ] 18 | 19 | 00:02:28.446 --> 00:02:29.916 A:middle 20 | 因此从这个视频可以看出 21 | 22 | 00:02:29.916 --> 00:02:31.926 A:middle 23 | 这系列影片生动的刻画了 24 | 25 | 00:02:31.926 --> 00:02:33.876 A:middle 26 | 当你为每个人 27 | 28 | 00:02:33.876 --> 00:02:35.856 A:middle 29 | 包括一定程度残疾的人做设计时 30 | 31 | 00:02:35.856 --> 00:02:37.886 A:middle 32 | 这些可用的科技 33 | 34 | 00:02:37.886 --> 00:02:39.266 A:middle 35 | 会对人们的生活造成多么大的影响 36 | 37 | 00:02:39.996 --> 00:02:41.956 A:middle 38 | 我今天在这里 39 | 40 | 00:02:41.956 --> 00:02:43.856 A:middle 41 | 是希望你们加入我们 42 | 43 | 00:02:43.856 --> 00:02:45.936 A:middle 44 | 设计人人可用的 45 | 46 | 00:02:45.936 --> 00:02:47.176 A:middle 47 | App 和游戏 48 | 49 | 00:02:47.976 --> 00:02:50.326 A:middle 50 | 因为我们的天性是 51 | 52 | 00:02:50.326 --> 00:02:52.166 A:middle 53 | 为自己做设计 54 | 55 | 00:02:52.166 --> 00:02:54.076 A:middle 56 | 设计反映出我们的见闻觉知 57 | 58 | 00:02:54.076 --> 00:02:54.556 A:middle 59 | 反映出我们的世界观 60 | 61 | 00:02:55.196 --> 00:02:56.616 A:middle 62 | 我们有需求 63 | 64 | 00:02:56.616 --> 00:02:57.266 A:middle 65 | 和激情 66 | 67 | 00:02:57.646 --> 00:02:59.086 A:middle 68 | 我们想出点子制作 App 69 | 70 | 00:02:59.556 --> 00:03:01.236 A:middle 71 | 加入公司 72 | 73 | -------------------------------------------------------------------------------- /Tests/WWDCHelperKitTests/WWDCHelperKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDCHelperKitTests.swift 3 | // WWDCHelperKitTests 4 | // 5 | // Created by kingcos on 06/09/2017. 6 | // 7 | // 8 | 9 | import XCTest 10 | 11 | import Spectre 12 | import PathKit 13 | 14 | @testable import WWDCHelperKit 15 | 16 | class WWDCHelperKitTests: XCTestCase { 17 | func testRunSpecture() { 18 | describe("----- WWDCHelpKit Tests -----") { 19 | 20 | let fixturesFolderPath = Path(#file).parent().parent() + "Fixtures" 21 | 22 | $0.describe("--- WWDC 2017 Parser ---") { 23 | let parser = WWDCParser.shared 24 | 25 | $0.it("should parse subtitle index URL prefix") { 26 | let content = "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_hd_platforms_state_of_the_union.mp4?dl=1" 27 | let result = parser.parseSubtitleIndexURLPrefix(in: content) 28 | let expectResult = "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102" 29 | 30 | try expect(expectResult) == result 31 | } 32 | 33 | $0.it("should parse resources") { 34 | let content = "
" 35 | let result = parser.parseResourceURLs(in: content) 36 | let expectResult = [ 37 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_hd_platforms_state_of_the_union.mp4", 38 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_sd_platforms_state_of_the_union.mp4", 39 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_platforms_state_of_the_union.pdf" 40 | ] 41 | 42 | try expect(expectResult) == result 43 | } 44 | 45 | $0.it("should parse sessions info") { 46 | let content = "\n

Advanced Animations with UIKit

\n
" 47 | let result = parser.parseSessionsInfo(in: content) 48 | let expectResult = [ 49 | "230": "Advanced Animations with UIKit" 50 | ] 51 | 52 | try expect(expectResult) == result 53 | } 54 | } 55 | 56 | $0.describe("--- Network ---") { 57 | $0.it("should fetch content") { 58 | let sampleHTML = "SampleContent.html" 59 | let url = (fixturesFolderPath + sampleHTML).url 60 | let result = Network.shared.fetchContent(of: url.absoluteString) 61 | let expectResult = try! String(contentsOf: url) 62 | 63 | try expect(expectResult) == result 64 | } 65 | } 66 | 67 | $0.describe("--- WWDC Helper ---") { 68 | var helper = WWDCHelper(year: "2017") 69 | var resourceURLs = [String]() 70 | 71 | $0.it("should get sessions info") { 72 | let result = helper.getSessionsInfo(with: WWDCParser.shared).keys.count 73 | let expectResult = 135 74 | 75 | try expect(expectResult) == result 76 | } 77 | 78 | $0.it("should get resource URLs") { 79 | let result = helper.getResourceURLs(by: "102", with: WWDCParser.shared) 80 | let expectResult = [ 81 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_hd_platforms_state_of_the_union.mp4", 82 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_sd_platforms_state_of_the_union.mp4", 83 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_platforms_state_of_the_union.pdf" 84 | ] 85 | 86 | resourceURLs = result 87 | 88 | try expect(expectResult) == result 89 | } 90 | 91 | $0.it("should get subtitle index URL prefix") { 92 | let result = helper.getSubtitleIndexURLPrefix(with: resourceURLs, 93 | and: WWDCParser.shared) ?? "" 94 | let expectResult = "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102" 95 | 96 | try expect(expectResult) == result 97 | } 98 | 99 | $0.it("should get subtitle index URL") { 100 | let result = helper.getSubtitleIndexURL(with: resourceURLs, 101 | and: WWDCParser.shared) ?? "" 102 | let expectResult = "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/subtitles/eng/prog_index.m3u8" 103 | 104 | try expect(expectResult) == result 105 | } 106 | 107 | $0.it("should get WebVTT URLs") { 108 | let result = helper.getWebVTTURLs(with: resourceURLs, 109 | and: WWDCParser.shared)?.count ?? 0 110 | let expectResult = 104 111 | 112 | try expect(expectResult) == result 113 | } 114 | 115 | $0.it("should get one session") { 116 | let result = try! helper.getSession(by: "102", 117 | with: WWDCParser.shared)! 118 | let expectResult = WWDCSession("102", 119 | "Platforms State of the Union", 120 | ["https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_hd_platforms_state_of_the_union.mp4", 121 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_sd_platforms_state_of_the_union.mp4", 122 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_platforms_state_of_the_union.pdf"], 123 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/subtitles/eng/prog_index.m3u8") 124 | 125 | try expect(expectResult) == result 126 | } 127 | 128 | $0.it("should get sessions") { 129 | /*let result = try! helper.getSessions().count 130 | let expectResult = 138 131 | 132 | try expect(expectResult) == result*/ 133 | } 134 | 135 | $0.it("should print sessions") { 136 | let session1 = WWDCSession("102", 137 | "Platforms State of the Union", 138 | ["https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_hd_platforms_state_of_the_union.mp4?dl=1", 139 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_sd_platforms_state_of_the_union.mp4?dl=1", 140 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_platforms_state_of_the_union.pdf?dl=1"], 141 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/subtitles/eng/prog_index.m3u8") 142 | let session2 = WWDCSession("102", 143 | "Platforms State of the Union", 144 | ["", 145 | "", 146 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/102_platforms_state_of_the_union.pdf?dl=1"], 147 | "https://devstreaming-cdn.apple.com/videos/wwdc/2017/102xyar2647hak3e/102/subtitles/eng/prog_index.m3u8") 148 | 149 | _ = [session1, session2].map { $0.output(helper.year) } 150 | } 151 | 152 | $0.it("should enter helper, then print sessions") { 153 | /* 154 | var helper = WWDCHelper() 155 | try! helper.enterHelper() 156 | 157 | helper = WWDCHelper(year: "2017") 158 | try! helper.enterHelper() 159 | 160 | helper = WWDCHelper(year: "2017", sessionIDs: ["102", "802"]) 161 | try! helper.enterHelper() 162 | */ 163 | helper = WWDCHelper(year: "2017", sessionIDs: ["202"]) 164 | try! helper.enterHelper() 165 | } 166 | 167 | $0.it("should enter helper, then print sessions & download subtitle") { 168 | /* 169 | helper = WWDCHelper(sessionIDs: ["202"], subtitleLanguage: "chs", subtitlePath: "./resources") 170 | try! helper.enterHelper() 171 | */ 172 | } 173 | } 174 | } 175 | } 176 | } 177 | 178 | public func ==(lhs: Expectation, rhs: WWDCSession) throws { 179 | if let left = try lhs.expression() { 180 | if left != rhs { 181 | throw lhs.failure("\(String(describing: left)) is not equal to \(rhs)") 182 | } 183 | 184 | } else { 185 | throw lhs.failure("given value is nil") 186 | } 187 | } 188 | 189 | public func ==(lhs: Expectation<[WWDCSession]>, rhs: [WWDCSession]) throws { 190 | if let left = try lhs.expression() { 191 | guard left.count == rhs.count else { 192 | throw lhs.failure("\(String(describing: left)) is not equal to \(rhs)") 193 | } 194 | for i in 0 ..< left.count { 195 | if left[i] != rhs[i] { 196 | throw lhs.failure("\(String(describing: left)) is not equal to \(rhs)") 197 | } 198 | } 199 | } else { 200 | throw lhs.failure("given value is nil") 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Tests/WWDCWebVTTToSRTHelperKitTests/WWDCWebVTTToSRTHelperKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDCWebVTTToSRTHelperKitTests.swift 3 | // WWDCWebVTTToSRTHelperKitTests 4 | // 5 | // Created by kingcos on 09/09/2017. 6 | // 7 | // 8 | 9 | import XCTest 10 | 11 | import Spectre 12 | import PathKit 13 | 14 | @testable import WWDCWebVTTToSRTHelperKit 15 | 16 | class WWDCWebVTTToSRTHelperKitTests: XCTestCase { 17 | func testRunSpecture() { 18 | describe("----- WWDCWebVTTToSRTHelperKit Tests -----") { 19 | 20 | let fixturesFolderPath = Path(#file).parent().parent() + "Fixtures" 21 | 22 | $0.describe("--- WebVTT Parser ---") { 23 | 24 | let parser = WWDCWebVTTParser() 25 | var contentArr = ["WEBVTT", 26 | "X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000", 27 | "", 28 | "01:42:58.766 --> 01:43:00.516 A:middle", 29 | ">> And with that, I hope you have a <<", 30 | "", 31 | "WEBVTT", 32 | "X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000", 33 | "", 34 | "01:43:00.516 --> 01:43:01.546 A:middle", 35 | "great conference, and I'll see", 36 | "you & them around this week.", 37 | ""] 38 | 39 | $0.it("should remove header") { 40 | parser.removeHeader(&contentArr) 41 | let expectResult = ["", 42 | "", 43 | "", 44 | "01:42:58.766 --> 01:43:00.516 A:middle", 45 | ">> And with that, I hope you have a <<", 46 | "", 47 | "", 48 | "", 49 | "", 50 | "01:43:00.516 --> 01:43:01.546 A:middle", 51 | "great conference, and I'll see", 52 | "you & them around this week.", 53 | ""] 54 | 55 | try expect(expectResult) == contentArr 56 | } 57 | 58 | $0.it("should remove blank lines") { 59 | parser.removeBlankLines(&contentArr) 60 | let expectResult = ["01:42:58.766 --> 01:43:00.516 A:middle", 61 | ">> And with that, I hope you have a <<", 62 | "01:43:00.516 --> 01:43:01.546 A:middle", 63 | "great conference, and I'll see", 64 | "you & them around this week."] 65 | 66 | try expect(expectResult) == contentArr 67 | } 68 | 69 | $0.it("should add line numbers") { 70 | parser.addLineNumbers(&contentArr) 71 | let expectResult = ["1", 72 | "01:42:58.766 --> 01:43:00.516 A:middle", 73 | ">> And with that, I hope you have a <<", 74 | "", 75 | "2", 76 | "01:43:00.516 --> 01:43:01.546 A:middle", 77 | "great conference, and I'll see", 78 | "you & them around this week."] 79 | 80 | try expect(expectResult) == contentArr 81 | } 82 | 83 | $0.it("should deal with lines") { 84 | parser.dealWithLines(&contentArr) 85 | let expectResult = ["1", 86 | "01:42:58,766 --> 01:43:00,516", 87 | ">> And with that, I hope you have a <<", 88 | "", 89 | "2", 90 | "01:43:00,516 --> 01:43:01,546", 91 | "great conference, and I'll see", 92 | "you & them around this week."] 93 | 94 | try expect(expectResult) == contentArr 95 | } 96 | 97 | $0.it("should replace characters") { 98 | parser.replaceCharacters(&contentArr) 99 | let expectResult = ["1", 100 | "01:42:58,766 --> 01:43:00,516", 101 | ">> And with that, I hope you have a <<", 102 | "", 103 | "2", 104 | "01:43:00,516 --> 01:43:01,546", 105 | "great conference, and I'll see", 106 | "you & them around this week."] 107 | 108 | try expect(expectResult) == contentArr 109 | } 110 | 111 | $0.it("should replace characters") { 112 | parser.replaceCharacters(&contentArr) 113 | let expectResult = ["1", 114 | "01:42:58,766 --> 01:43:00,516", 115 | ">> And with that, I hope you have a <<", 116 | "", 117 | "2", 118 | "01:43:00,516 --> 01:43:01,546", 119 | "great conference, and I'll see", 120 | "you & them around this week."] 121 | 122 | try expect(expectResult) == contentArr 123 | } 124 | 125 | $0.it("should parse to SRT") { 126 | var urls = [URL]() 127 | for i in 0 ..< 3 { 128 | urls.append((fixturesFolderPath + "fileSequence\(i).webvtt").url) 129 | } 130 | 131 | let strArr = urls.map { try! String(contentsOf: $0) } 132 | 133 | guard let string = parser.parseToSRT(strArr) else { return } 134 | let result = string.components(separatedBy: "\n").count 135 | let expectResult = 328 136 | 137 | try expect(expectResult) == result 138 | } 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /WWDCHelper-h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingcos/WWDCHelper/c88f09856359d42155bdcb09e40283997889de12/WWDCHelper-h.png -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: header, changes, diff 3 | coverage: 4 | ignore: 5 | - Tests 6 | - resources 7 | - CI 8 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "WWDCHelper will be installed for a litter while, please take a break~\n" 3 | swift package clean 4 | swift test 5 | swift build -c release 6 | cp .build/release/WWDCHelper /usr/local/bin/wwdchelper 7 | cd .. 8 | rm -rf WWDCHelper 9 | echo "\n WWDCHelper has been installed successfully, try `wwdchelper -h`~" -------------------------------------------------------------------------------- /resources/202_hd_advances_in_tvmlkit.chs.srt: -------------------------------------------------------------------------------- 1 | 2 | 1 3 | 00:00:20,516 --> 00:00:22,916 4 | [掌声] 5 | 2 6 | 00:00:23,416 --> 00:00:23,596 7 | >> 早上好 8 | 3 9 | 00:00:25,956 --> 00:00:28,146 10 | 欢迎来听“TVMLKit 的新改进” 11 | 4 12 | 00:00:28,526 --> 00:00:29,146 13 | 我叫 Trevor 14 | 5 15 | 00:00:29,146 --> 00:00:30,486 16 | 是本地化部门的员工 17 | 6 18 | 00:00:30,486 --> 00:00:31,526 19 | 我的同事 来自 tvOS 部门的 20 | 7 21 | 00:00:31,526 --> 00:00:32,936 22 | Parry 和 Jeremy 会在我 23 | 8 24 | 00:00:32,936 --> 00:00:33,866 25 | 之后发言 26 | 9 27 | 00:00:34,366 --> 00:00:35,656 28 | 今天 我们很高兴 29 | 10 30 | 00:00:35,656 --> 00:00:37,146 31 | 能和大家分享我们对 TVMLKit 32 | 11 33 | 00:00:37,146 --> 00:00:39,586 34 | 做出的三大新改进 35 | 12 36 | 00:00:39,586 --> 00:00:41,226 37 | TVMLKit 是一个开发者框架 38 | 13 39 | 00:00:41,226 --> 00:00:42,586 40 | 用于为 Apple TV 开发 41 | 14 42 | 00:00:42,586 --> 00:00:45,246 43 | 原生应用程序 该框架 44 | 15 45 | 00:00:45,466 --> 00:00:47,356 46 | 可整合 JavaScript 和苹果自定义 47 | 16 48 | 00:00:47,356 --> 00:00:48,646 49 | 标记语言 TVML 50 | 17 51 | 00:00:51,376 --> 00:00:52,716 52 | 首先 演讲内容会谈到 53 | 18 54 | 00:00:52,716 --> 00:00:54,146 55 | 对从右往左书写文字的支持 56 | 19 57 | 00:00:55,166 --> 00:00:56,866 58 | 第二位演讲者 Parry 会给大家 59 | 20 60 | 00:00:56,866 --> 00:00:57,876 61 | 展示如何应用模版中 62 | 21 63 | 00:00:57,876 --> 00:00:59,426 64 | 一些新增的优化功能 65 | 22 66 | 00:00:59,696 --> 00:01:01,066 67 | 来提升程序性能 68 | 23 69 | 00:00:59,696 --> 00:01:01,066 70 | 来提升程序性能 71 | 24 72 | 00:01:02,016 --> 00:01:03,456 73 | 最后 Jeremy 会 74 | 25 75 | 00:01:03,456 --> 00:01:04,995 76 | 给大家展示 Web 审查器中 77 | 26 78 | 00:01:04,995 --> 00:01:06,376 79 | 新增了哪些很棒的新功能 80 | 27 81 | 00:01:06,436 --> 00:01:07,286 82 | 可帮你更便捷地 83 | 28 84 | 00:01:07,286 --> 00:01:08,826 85 | 给你的程序调试排错 86 | 29 87 | 00:01:09,316 --> 00:01:10,636 88 | 下面我们就开始 我先讲 89 | 30 90 | 00:01:10,636 --> 00:01:11,896 91 | 对从右往左书写文字的支持 92 | 31 93 | 00:01:14,836 --> 00:01:16,456 94 | tvOS 11 中新增了 95 | 32 96 | 00:01:16,456 --> 00:01:17,996 97 | 两个可选语言 分别是阿拉伯语 98 | 33 99 | 00:01:17,996 --> 00:01:18,946 100 | 和希伯来语 101 | 34 102 | 00:01:20,206 --> 00:01:22,386 103 | 两门语言全球有超过4亿人使用 104 | 35 105 | 00:01:22,386 --> 00:01:23,806 106 | 如果你的 App 107 | 36 108 | 00:01:23,806 --> 00:01:25,436 109 | 能够在这两门语言环境下运行 110 | 37 111 | 00:01:25,666 --> 00:01:27,106 112 | 就可让各位开发的应用触及到更多的用户 113 | 38 114 | 00:01:28,416 --> 00:01:29,626 115 | 要支持阿拉伯语和希伯来语 116 | 39 117 | 00:01:29,626 --> 00:01:30,846 118 | 对比大家的应用程序目前 119 | 40 120 | 00:01:30,846 --> 00:01:31,986 121 | 已经支持的其他语言 122 | 41 123 | 00:01:31,986 --> 00:01:33,556 124 | 区别在于阿拉伯语和希伯来语 125 | 42 126 | 00:01:33,556 --> 00:01:35,156 127 | 都是从右往左书写的 128 | 43 129 | 00:01:35,196 --> 00:01:35,646 130 | 文字 131 | 44 132 | 00:01:36,686 --> 00:01:37,646 133 | 我先给大家看几个例子 展示一下 134 | 45 135 | 00:01:37,646 --> 00:01:38,786 136 | 支持从右往左书写语言后 137 | 46 138 | 00:01:38,786 --> 00:01:40,066 139 | 大家的应用程序 看起来 140 | 47 141 | 00:01:40,066 --> 00:01:41,036 142 | 大概什么样 143 | 48 144 | 00:01:45,056 --> 00:01:46,846 145 | 现在显示的是一个 146 | 49 147 | 00:01:46,846 --> 00:01:47,926 148 | 从左往右书写语言模式下 149 | 50 150 | 00:01:47,926 --> 00:01:50,126 151 | 的产品模版 大部分人 152 | 51 153 | 00:01:50,126 --> 00:01:51,256 154 | 可能已经很熟悉这个 155 | 52 156 | 00:01:51,256 --> 00:01:51,826 157 | 界面了 158 | 53 159 | 00:01:51,916 --> 00:01:53,446 160 | 文字左对齐 161 | 54 162 | 00:01:53,796 --> 00:01:55,226 163 | 内容排布顺序 164 | 55 165 | 00:01:55,226 --> 00:01:56,376 166 | 是从屏幕左边 167 | 56 168 | 00:01:56,376 --> 00:01:57,426 169 | 排向屏幕右边 170 | 57 171 | 00:01:58,006 --> 00:01:59,126 172 | 优先选中的第一个按钮 173 | 58 174 | 00:01:59,126 --> 00:02:00,186 175 | 也是在屏幕的 176 | 59 177 | 00:01:59,126 --> 00:02:00,186 178 | 也是在屏幕的 179 | 60 180 | 00:02:00,186 --> 00:02:00,966 181 | 最左边 182 | 61 183 | 00:02:02,096 --> 00:02:03,446 184 | 现在 如果我们选用这个模版 185 | 62 186 | 00:02:03,896 --> 00:02:04,846 187 | 然后在从右往左 188 | 63 189 | 00:02:04,846 --> 00:02:06,376 190 | 书写文字模式下加载 191 | 64 192 | 00:02:06,376 --> 00:02:08,406 193 | 比如选择希伯来语 就可以看到 194 | 65 195 | 00:02:08,406 --> 00:02:09,466 196 | 页面布局被左右翻转了 197 | 66 198 | 00:02:09,466 --> 00:02:11,606 199 | 内容排布顺序会变成 200 | 67 201 | 00:02:11,606 --> 00:02:12,786 202 | 从屏幕右边 203 | 68 204 | 00:02:12,786 --> 00:02:13,526 205 | 排向屏幕左边 206 | 69 207 | 00:02:13,826 --> 00:02:15,146 208 | 文字右对齐 209 | 70 210 | 00:02:15,646 --> 00:02:17,106 211 | 优先选中的 还是第一个按钮 212 | 71 213 | 00:02:17,106 --> 00:02:18,326 214 | 但现在这一项位于 215 | 72 216 | 00:02:18,326 --> 00:02:19,016 217 | 屏幕右边了 218 | 73 219 | 00:02:19,506 --> 00:02:21,386 220 | 我们再看一个例子 221 | 74 222 | 00:02:23,296 --> 00:02:24,976 223 | 这是目录模版 224 | 75 225 | 00:02:24,976 --> 00:02:27,146 226 | 和刚才一样 如果你常用的语言 227 | 76 228 | 00:02:27,146 --> 00:02:28,646 229 | 是从左往右书写的 230 | 77 231 | 00:02:28,646 --> 00:02:29,986 232 | 你可能会很熟悉这个界面 233 | 78 234 | 00:02:29,986 --> 00:02:30,536 235 | 很自然 236 | 79 237 | 00:02:30,976 --> 00:02:32,026 238 | 但如果我们把同一个模版 239 | 80 240 | 00:02:32,026 --> 00:02:33,396 241 | 放到从右往左书写文字的 242 | 81 243 | 00:02:33,396 --> 00:02:35,126 244 | 模式下 我们会发现 245 | 82 246 | 00:02:35,126 --> 00:02:36,676 247 | 页面布局同样被翻转了 248 | 83 249 | 00:02:36,866 --> 00:02:38,196 250 | 内容变成从右往左 251 | 84 252 | 00:02:38,196 --> 00:02:38,626 253 | 排布 254 | 85 255 | 00:02:38,836 --> 00:02:40,016 256 | 第一张图片依然是沙漠 257 | 86 258 | 00:02:40,016 --> 00:02:41,106 259 | 但现在这张图片显示在 260 | 87 261 | 00:02:41,106 --> 00:02:41,876 262 | 屏幕的另一边 263 | 88 264 | 00:02:42,876 --> 00:02:44,136 265 | 从右往左书写文字的模式下 266 | 89 267 | 00:02:44,136 --> 00:02:46,056 268 | 应用程序的界面看起来差不多就是这样 269 | 90 270 | 00:02:46,346 --> 00:02:47,276 271 | 从刚刚的演示中也可看到 272 | 91 273 | 00:02:47,276 --> 00:02:48,416 274 | 我们把对相应语言的支持 275 | 92 276 | 00:02:48,416 --> 00:02:49,896 277 | 都整合到了 TVMLKit 内部了 278 | 93 279 | 00:02:50,416 --> 00:02:52,036 280 | 所以 只要你使用的是默认模版 281 | 94 282 | 00:02:52,036 --> 00:02:53,306 283 | 你们的应用程序就可免费获得 284 | 95 285 | 00:02:53,306 --> 00:02:55,836 286 | 对从右往左书写文字的优化支持 287 | 96 288 | 00:02:56,006 --> 00:02:57,946 289 | 而要让应用程序支持 290 | 97 291 | 00:02:57,946 --> 00:02:59,756 292 | 从右往左书写文字的显示模式 293 | 98 294 | 00:02:59,756 --> 00:03:01,026 295 | 设置方法 和配置其他支持语言 296 | 99 297 | 00:02:59,756 --> 00:03:01,026 298 | 设置方法 和配置其他支持语言 299 | 100 300 | 00:03:01,026 --> 00:03:01,486 301 | 方法一样 302 | 101 303 | 00:03:02,176 --> 00:03:03,816 304 | 只需在 Xcode 中 305 | 102 306 | 00:03:03,816 --> 00:03:05,646 307 | 进入 Project Setting 页面 308 | 103 309 | 00:03:05,646 --> 00:03:07,996 310 | 可看到 在 localizations 下 311 | 104 312 | 00:03:07,996 --> 00:03:09,336 313 | 目前新增了阿语和希语 314 | 105 315 | 00:03:09,336 --> 00:03:10,366 316 | 我们按需添加即可 317 | 106 318 | 00:03:12,076 --> 00:03:13,946 319 | 把两种语言整合到 TVMLKit 中很重要 320 | 107 321 | 00:03:13,946 --> 00:03:15,696 322 | 这样可确保 TVMLKit 在运行时 323 | 108 324 | 00:03:15,696 --> 00:03:16,616 325 | 能判断何时加载成 326 | 109 327 | 00:03:16,616 --> 00:03:17,786 328 | 从右往左显示的界面 329 | 110 330 | 00:03:18,496 --> 00:03:19,846 331 | 关于如何更好地 332 | 111 333 | 00:03:19,846 --> 00:03:20,866 334 | 把应用程序本地化 335 | 112 336 | 00:03:20,866 --> 00:03:22,146 337 | 和更多相关详细信息 338 | 113 339 | 00:03:22,146 --> 00:03:23,396 340 | 包括内容本地化 341 | 114 342 | 00:03:23,396 --> 00:03:24,516 343 | 和如何设置本地化的 344 | 115 345 | 00:03:24,516 --> 00:03:25,946 346 | 时间日期格式 等等 347 | 116 348 | 00:03:26,306 --> 00:03:27,606 349 | 我推荐大家去听一个 350 | 117 351 | 00:03:27,606 --> 00:03:29,266 352 | 今年的演讲 叫“Localizing With X 353 | 118 354 | 00:03:29,266 --> 00:03:30,646 355 | Code 9” 那里会讲得 356 | 119 357 | 00:03:30,646 --> 00:03:33,086 358 | 更详细一些 359 | 120 360 | 00:03:33,226 --> 00:03:35,036 361 | 好了 刚说了使用默认模版自带的 362 | 121 363 | 00:03:35,036 --> 00:03:36,856 364 | 设置一键配置本地化 但如果程序里 365 | 122 366 | 00:03:36,856 --> 00:03:38,526 367 | 有自定义界面呢 如果你想 368 | 123 369 | 00:03:38,526 --> 00:03:39,946 370 | 实现一些模版中没有提供的 371 | 124 372 | 00:03:39,946 --> 00:03:40,696 373 | 显示效果呢 374 | 125 375 | 00:03:41,306 --> 00:03:42,666 376 | 在 tvOS 11 里 377 | 126 378 | 00:03:42,666 --> 00:03:44,456 379 | 我们新引入了三处可以 380 | 127 381 | 00:03:44,456 --> 00:03:46,956 382 | 自定义的领域 分别是页面布局 383 | 128 384 | 00:03:47,536 --> 00:03:49,316 385 | 文字对齐方式和图片显示 386 | 129 387 | 00:03:49,706 --> 00:03:51,356 388 | 我们首先说页面布局 389 | 130 390 | 00:03:52,556 --> 00:03:54,296 391 | 目前我们自定义页面布局 392 | 131 393 | 00:03:54,296 --> 00:03:56,456 394 | 可能是靠分别设置 395 | 132 396 | 00:03:56,456 --> 00:03:57,476 397 | tv-align 为 left(左) 398 | 133 399 | 00:03:57,476 --> 00:03:59,446 400 | 和设置 tv-position 401 | 134 402 | 00:03:59,446 --> 00:03:59,846 403 | 为 right(右) 404 | 135 405 | 00:04:01,046 --> 00:04:01,946 406 | 而对于从右往左书写 407 | 136 408 | 00:04:01,946 --> 00:04:03,836 409 | 的文字 左是右 410 | 137 411 | 00:04:03,836 --> 00:04:05,516 412 | 右是左 是颠倒的 413 | 138 414 | 00:04:05,516 --> 00:04:07,366 415 | 这样我们再使用 left 和 right 就得格外小心 416 | 139 417 | 00:04:07,366 --> 00:04:10,076 418 | 很容易设置错方向 419 | 140 420 | 00:04:10,266 --> 00:04:11,436 421 | 为此 我们引入了两个新的属性 422 | 141 423 | 00:04:11,436 --> 00:04:12,446 424 | 叫 leading 和 trailing 425 | 142 426 | 00:04:13,496 --> 00:04:14,496 427 | Leading 和 trailing 428 | 143 429 | 00:04:14,496 --> 00:04:15,796 430 | 会在程序运行时 431 | 144 432 | 00:04:15,796 --> 00:04:17,336 433 | 根据运行时的语言环境 434 | 145 435 | 00:04:17,336 --> 00:04:18,676 436 | 自动解析出与之适应的 left 437 | 146 438 | 00:04:18,676 --> 00:04:19,486 439 | 或者 right 440 | 147 441 | 00:04:19,486 --> 00:04:20,055 442 | 值 443 | 148 444 | 00:04:20,886 --> 00:04:22,096 445 | 比方说 如果我们 446 | 149 447 | 00:04:22,096 --> 00:04:23,156 448 | 本来定义的样式里是 449 | 150 450 | 00:04:23,156 --> 00:04:24,806 451 | 把 tv-position 设为 right 452 | 151 453 | 00:04:25,116 --> 00:04:26,946 454 | 或者 tv-align 设为 left 455 | 152 456 | 00:04:26,946 --> 00:04:28,216 457 | 现在 我们需要把后面的值 458 | 153 459 | 00:04:28,216 --> 00:04:29,246 460 | 改为 leading 和 trailing 461 | 154 462 | 00:04:29,876 --> 00:04:31,336 463 | 这样就可以确保 464 | 155 465 | 00:04:31,336 --> 00:04:32,506 466 | 在从左往右语言模式下 467 | 156 468 | 00:04:32,506 --> 00:04:33,716 469 | 页面显示效果还和原来一样 470 | 157 471 | 00:04:33,716 --> 00:04:35,596 472 | 但用从右往左语言模式加载后 473 | 158 474 | 00:04:35,596 --> 00:04:37,016 475 | 内容依然能正确排布 476 | 159 477 | 00:04:40,116 --> 00:04:41,726 478 | 对于 margin 和 padding 的赋值 479 | 160 480 | 00:04:41,726 --> 00:04:43,166 481 | tvOS 11 引入了 482 | 161 483 | 00:04:43,166 --> 00:04:45,386 484 | 一个全新的媒体查询属性 485 | 162 486 | 00:04:45,836 --> 00:04:47,336 487 | 叫作 Layout Direction (布局方向) 488 | 163 489 | 00:04:48,056 --> 00:04:49,366 490 | 大家可能已经 491 | 164 492 | 00:04:49,366 --> 00:04:51,166 493 | 在通过使用媒体查询 494 | 165 495 | 00:04:51,166 --> 00:04:52,336 496 | 来自定义调整页面明暗 497 | 166 498 | 00:04:52,976 --> 00:04:54,166 499 | 这里也是一样的道理 500 | 167 501 | 00:04:54,606 --> 00:04:56,016 502 | 在媒体查询内部 503 | 168 504 | 00:04:56,016 --> 00:04:57,906 505 | 定义的样式 只在当前 506 | 169 507 | 00:04:57,906 --> 00:04:59,706 508 | 媒体查询中有效 509 | 170 510 | 00:05:00,306 --> 00:05:02,336 511 | 如何应用 我来举一个例子 512 | 171 513 | 00:05:02,506 --> 00:05:04,786 514 | 这个媒体查询定义了两个样式 515 | 172 516 | 00:05:04,786 --> 00:05:06,406 517 | 一个适用于从左往右(LTR)方向 518 | 173 519 | 00:05:06,406 --> 00:05:08,256 520 | 另一个适用于从右往左(RTL)方向 521 | 174 522 | 00:05:09,356 --> 00:05:11,396 523 | 这里设置 margin 时需要特意留心 524 | 175 525 | 00:05:11,396 --> 00:05:13,466 526 | 依据语言书写方向 527 | 176 528 | 00:05:13,466 --> 00:05:15,106 529 | 把水平向的左右外边距值 530 | 177 531 | 00:05:15,106 --> 00:05:16,016 532 | 作相应对调 533 | 178 534 | 00:05:16,376 --> 00:05:17,556 535 | 在这个例子中 536 | 179 537 | 00:05:17,556 --> 00:05:19,136 538 | 我们给 margin 设置的值 12 539 | 180 540 | 00:05:19,186 --> 00:05:20,756 541 | 就要写在正确的位置 542 | 181 543 | 00:05:21,216 --> 00:05:23,556 544 | 页面布局就说到这里 545 | 182 546 | 00:05:23,556 --> 00:05:25,356 547 | 那么文字对齐方向呢 548 | 183 549 | 00:05:26,216 --> 00:05:28,446 550 | 我们通常习惯的是 统一设置 551 | 184 552 | 00:05:28,446 --> 00:05:30,436 553 | 左对齐 居中 或者 554 | 185 555 | 00:05:30,436 --> 00:05:30,856 556 | 右对齐 557 | 186 558 | 00:05:31,316 --> 00:05:32,526 559 | 而我们看这个屏幕上的文字 560 | 187 561 | 00:05:32,526 --> 00:05:34,536 562 | 其中分别有两种显示方式 563 | 188 564 | 00:05:34,536 --> 00:05:35,826 565 | 看起来比其他的更自然 566 | 189 567 | 00:05:35,826 --> 00:05:37,676 568 | 对于从左往右书写的文字 569 | 190 570 | 00:05:37,676 --> 00:05:39,086 571 | 左对齐看起来自然 572 | 191 573 | 00:05:39,086 --> 00:05:40,776 574 | 对于从右往左书写的文字 575 | 192 576 | 00:05:40,776 --> 00:05:42,066 577 | 右对齐看起来自然 578 | 193 579 | 00:05:43,166 --> 00:05:45,216 580 | 为此 我们专门 581 | 194 582 | 00:05:45,216 --> 00:05:46,436 583 | 与市场团队碰头协商 584 | 195 585 | 00:05:46,436 --> 00:05:48,666 586 | 思考了一下 究竟该如何 587 | 196 588 | 00:05:48,736 --> 00:05:50,256 589 | 命名这种对齐方式 590 | 197 591 | 00:05:50,256 --> 00:05:51,596 592 | 最终决定 采用一个新的值 593 | 198 594 | 00:05:51,596 --> 00:05:52,826 595 | 叫作 自然对齐 596 | 199 597 | 00:05:53,716 --> 00:05:55,096 598 | 自然对齐 效果和大家想象的 599 | 200 600 | 00:05:55,096 --> 00:05:55,936 601 | 差不多 602 | 201 603 | 00:05:55,936 --> 00:05:57,616 604 | 会根据大家应用程序的 UI 语言 605 | 202 606 | 00:05:57,616 --> 00:05:59,006 607 | 对文字进行自然对齐 608 | 203 609 | 00:05:59,506 --> 00:05:59,956 610 | 很简单 611 | 204 612 | 00:06:02,496 --> 00:06:04,156 613 | 下面再说应用程序中的图片显示 614 | 205 615 | 00:06:04,156 --> 00:06:06,126 616 | 大部分应用程序内采用的图片 617 | 206 618 | 00:06:06,126 --> 00:06:07,296 619 | 都有一定普适性 620 | 207 621 | 00:06:07,386 --> 00:06:08,586 622 | 不管页面如何布局 都可以 623 | 208 624 | 00:06:08,586 --> 00:06:09,566 625 | 正常显示 626 | 209 627 | 00:06:10,146 --> 00:06:12,076 628 | 但在个别情况下 629 | 210 630 | 00:06:12,076 --> 00:06:13,906 631 | 某些图片会自带方向属性 632 | 211 633 | 00:06:13,906 --> 00:06:15,336 634 | 比如这个表单项后面的 635 | 212 636 | 00:06:15,336 --> 00:06:15,956 637 | V 形标记 638 | 213 639 | 00:06:16,506 --> 00:06:17,636 640 | 这种情况下 641 | 214 642 | 00:06:17,636 --> 00:06:18,706 643 | 就需要确保 V 形箭头 644 | 215 645 | 00:06:18,706 --> 00:06:19,626 646 | 指向正确的方向 647 | 216 648 | 00:06:19,626 --> 00:06:21,436 649 | 不然整个列表页面就会 650 | 217 651 | 00:06:21,436 --> 00:06:22,896 652 | 令人困惑 看起来也 653 | 218 654 | 00:06:22,896 --> 00:06:23,366 655 | 很奇怪 656 | 219 657 | 00:06:24,226 --> 00:06:26,086 658 | 要在应用程序中做到这种效果 659 | 220 660 | 00:06:26,086 --> 00:06:27,316 661 | 有两种方法 662 | 221 663 | 00:06:27,626 --> 00:06:29,006 664 | 一种是针对资源 665 | 222 666 | 00:06:29,006 --> 00:06:29,576 667 | 图片 668 | 223 669 | 00:06:30,006 --> 00:06:31,186 670 | 对于来自应用程序 671 | 224 672 | 00:06:31,186 --> 00:06:32,626 673 | 捆绑包内部的图片 674 | 225 675 | 00:06:33,216 --> 00:06:35,916 676 | 可通过图片资源目录 (Asset Catalogs) 677 | 226 678 | 00:06:35,916 --> 00:06:37,576 679 | 来便捷地自定义 680 | 227 681 | 00:06:37,576 --> 00:06:39,706 682 | 图片显示方向 683 | 228 684 | 00:06:39,706 --> 00:06:41,946 685 | 可设置在所有布局下 686 | 229 687 | 00:06:41,946 --> 00:06:43,446 688 | 图片显示固定不变 689 | 230 690 | 00:06:43,446 --> 00:06:45,066 691 | 可像刚刚的 V 形标记 692 | 231 693 | 00:06:45,256 --> 00:06:46,946 694 | 在运行时左右翻转显示 695 | 232 696 | 00:06:46,946 --> 00:06:48,486 697 | 或是给每门语言都指定一张 698 | 233 699 | 00:06:48,486 --> 00:06:49,546 700 | 单独的图片 701 | 234 702 | 00:06:51,526 --> 00:06:53,896 703 | 对于来自于服务器上的图片 704 | 235 705 | 00:06:53,896 --> 00:06:56,916 706 | 我们引入了一个全新的属性 707 | 236 708 | 00:06:56,916 --> 00:06:58,246 709 | 可以用来定义图片元素 710 | 237 711 | 00:06:58,246 --> 00:06:59,396 712 | 这个属性就叫作 713 | 238 714 | 00:06:59,396 --> 00:06:59,906 715 | Source Set 716 | 239 717 | 00:07:01,226 --> 00:07:02,376 718 | Source Set 可针对 719 | 240 720 | 00:07:02,376 --> 00:07:04,906 721 | 某给定的布局方向设置一个 722 | 241 723 | 00:07:04,906 --> 00:07:05,706 724 | 既定的图片 URL 725 | 242 726 | 00:07:06,596 --> 00:07:07,826 727 | Source Set 还有一个好处 728 | 243 729 | 00:07:07,826 --> 00:07:09,116 730 | 就是 我们也可以通过 731 | 244 732 | 00:07:09,116 --> 00:07:10,706 733 | 设置 Source Set 属性来调整 734 | 245 735 | 00:07:10,706 --> 00:07:11,336 736 | 明暗度 737 | 246 738 | 00:07:12,006 --> 00:07:15,346 739 | 在这个例子中 当布局方向是 LTR 时 740 | 247 741 | 00:07:15,486 --> 00:07:17,126 742 | 就会载入 URL1 链接中的图片 743 | 248 744 | 00:07:17,896 --> 00:07:20,046 745 | 而当布局方向是 RTL 时 746 | 249 747 | 00:07:20,046 --> 00:07:21,296 748 | 就会载入 URL2 图片 749 | 250 750 | 00:07:21,296 --> 00:07:23,306 751 | 通过这种方法 752 | 251 753 | 00:07:23,306 --> 00:07:24,856 754 | 可以指定显示某张图片 755 | 252 756 | 00:07:24,856 --> 00:07:26,406 757 | 运行时就会产出 758 | 253 759 | 00:07:26,406 --> 00:07:29,006 760 | 正确的结果 761 | 254 762 | 00:07:29,096 --> 00:07:30,476 763 | 好了 现在看我们 764 | 255 765 | 00:07:30,476 --> 00:07:31,846 766 | 能不能综合以上谈到的所有点 767 | 256 768 | 00:07:31,846 --> 00:07:33,026 769 | 糅合在一个小程序里 770 | 257 771 | 00:07:33,026 --> 00:07:33,706 772 | 我做了一个样本给大家 773 | 258 774 | 00:07:33,706 --> 00:07:33,936 775 | 看看 776 | 259 777 | 00:07:44,386 --> 00:07:45,876 778 | 我之前一直在做一个应用程序 779 | 260 780 | 00:07:45,876 --> 00:07:48,186 781 | 来展示去年 WWDC 782 | 261 783 | 00:07:48,186 --> 00:07:49,516 784 | 会上的演讲 785 | 262 786 | 00:07:49,516 --> 00:07:51,096 787 | 我现在跟大家分享一下 788 | 263 789 | 00:07:51,096 --> 00:07:51,506 790 | 这个程序 791 | 264 792 | 00:08:02,316 --> 00:08:03,436 793 | 这个页面里 我们可以看到 794 | 265 795 | 00:08:03,436 --> 00:08:05,346 796 | 呈网格 (grid) 排列的往期演讲 797 | 266 798 | 00:08:05,346 --> 00:08:06,656 799 | 顶端我设置了一个横幅 800 | 267 801 | 00:08:06,656 --> 00:08:07,976 802 | 来进行置顶推荐 803 | 268 804 | 00:08:09,036 --> 00:08:11,026 805 | 在正式让我的应用支持 806 | 269 807 | 00:08:11,026 --> 00:08:12,106 808 | 从右往左书写语言之前 809 | 270 810 | 00:08:12,106 --> 00:08:14,116 811 | 不去设置里添加本地化语言 812 | 271 813 | 00:08:14,116 --> 00:08:15,026 814 | 也可以做一些此方面的 815 | 272 816 | 00:08:15,026 --> 00:08:15,786 817 | 其他尝试 818 | 273 819 | 00:08:16,926 --> 00:08:21,056 820 | 我们可以去 edit scheme 下 821 | 274 822 | 00:08:21,136 --> 00:08:22,966 823 | 在 run option 的下拉菜单中 824 | 275 825 | 00:08:22,966 --> 00:08:24,826 826 | 可以把应用程序 827 | 276 828 | 00:08:24,826 --> 00:08:25,966 829 | 的系统语言 830 | 277 831 | 00:08:25,966 --> 00:08:27,286 832 | 改为从右往左的伪码 833 | 278 834 | 00:08:27,836 --> 00:08:29,476 835 | 设置后就可以模拟 836 | 279 837 | 00:08:29,476 --> 00:08:30,366 838 | 程序在从右往左 839 | 280 840 | 00:08:30,366 --> 00:08:31,796 841 | 语言环境中的效果 842 | 281 843 | 00:08:31,796 --> 00:08:32,996 844 | 而项目本身不用做 845 | 282 846 | 00:08:32,996 --> 00:08:33,645 847 | 任何改动 848 | 283 849 | 00:08:34,856 --> 00:08:36,466 850 | 现在再编译并运行的话 851 | 284 852 | 00:08:36,946 --> 00:08:38,466 853 | 就能看到 网格布局变成了 854 | 285 855 | 00:08:38,466 --> 00:08:39,395 856 | 从右往左排布 857 | 286 858 | 00:08:39,816 --> 00:08:41,635 859 | 又因为我们使用了网格布局 860 | 287 861 | 00:08:41,635 --> 00:08:42,976 862 | 这些效果支持都是模版 863 | 288 864 | 00:08:42,976 --> 00:08:43,696 865 | 免费自带的 866 | 289 867 | 00:08:44,126 --> 00:08:45,616 868 | 但是因为我的横幅 869 | 290 870 | 00:08:45,616 --> 00:08:47,146 871 | 是我自己自定义的样式 872 | 291 873 | 00:08:47,146 --> 00:08:48,816 874 | 所以看起来这一部分还需要 875 | 292 876 | 00:08:48,816 --> 00:08:49,426 877 | 我再去调试一下 878 | 293 879 | 00:08:49,936 --> 00:08:51,436 880 | 现在就让我们尝试使用 881 | 294 882 | 00:08:51,436 --> 00:08:53,746 883 | tvOS 11 中的新 API 来调整 884 | 295 885 | 00:08:53,746 --> 00:08:54,196 886 | 一下 887 | 296 888 | 00:08:55,836 --> 00:08:57,056 889 | 首先我要做的一件事 就是 890 | 297 891 | 00:08:57,056 --> 00:08:59,126 892 | 检查我 TVML 里定义的样式 893 | 298 894 | 00:08:59,126 --> 00:09:01,126 895 | 然后把所有规定死的 896 | 299 897 | 00:08:59,126 --> 00:09:01,126 898 | 然后把所有规定死的 899 | 300 900 | 00:09:01,226 --> 00:09:03,936 901 | left 都替换成 leading 902 | 301 903 | 00:09:03,936 --> 00:09:05,656 904 | 在我所用的开发语言环境下 905 | 302 906 | 00:09:05,656 --> 00:09:07,036 907 | leading 也就是屏幕左侧 (left) 908 | 303 909 | 00:09:08,386 --> 00:09:09,836 910 | 同样的 也把所有的 911 | 304 912 | 00:09:09,836 --> 00:09:11,776 913 | right 都替换成 trailing 914 | 305 915 | 00:09:14,156 --> 00:09:15,636 916 | 现在我们再编译并运行应用程序 917 | 306 918 | 00:09:15,686 --> 00:09:17,176 919 | 应该会看到改进后有什么 920 | 307 921 | 00:09:17,176 --> 00:09:17,816 922 | 不同 923 | 308 924 | 00:09:19,696 --> 00:09:21,736 925 | 很好 看起来文字布局 926 | 309 927 | 00:09:21,736 --> 00:09:22,676 928 | 都翻到了屏幕另一边 929 | 310 930 | 00:09:22,676 --> 00:09:24,366 931 | 页面上的播放按钮 932 | 311 933 | 00:09:24,366 --> 00:09:25,366 934 | 也在正确的位置 935 | 312 936 | 00:09:25,676 --> 00:09:27,356 937 | 但播放按钮看起来 938 | 313 939 | 00:09:27,356 --> 00:09:29,896 940 | 离边框有点太近了 941 | 314 942 | 00:09:30,116 --> 00:09:31,996 943 | 查看一下代码样式就发现 944 | 315 945 | 00:09:31,996 --> 00:09:33,626 946 | 我为 bannerPlayButtonLockup 单独 947 | 316 948 | 00:09:33,626 --> 00:09:34,766 949 | 设置了 margin 但定义的 950 | 317 951 | 00:09:34,766 --> 00:09:35,706 952 | 是元素的右边距 953 | 318 954 | 00:09:36,016 --> 00:09:37,426 955 | 我们需要对此进行调整 956 | 319 957 | 00:09:37,426 --> 00:09:38,656 958 | 使得我们切换为从右往左语言模式后 959 | 320 960 | 00:09:38,656 --> 00:09:39,626 961 | margin 赋值也相应对调 962 | 321 963 | 00:09:41,096 --> 00:09:43,116 964 | 为了节省时间 我这里插入 965 | 322 966 | 00:09:43,116 --> 00:09:44,686 967 | 我预先写好的一个片段 968 | 323 969 | 00:09:44,686 --> 00:09:46,496 970 | 这里为 bannerPlayButtonLockup 定义了 971 | 324 972 | 00:09:46,496 --> 00:09:48,686 973 | LTR 和 RTL 两个情况下的媒体查询 974 | 325 975 | 00:09:48,816 --> 00:09:51,576 976 | 我只需把原先定义好的 margin 977 | 326 978 | 00:09:51,576 --> 00:09:54,646 979 | 移动到 LTR 媒体查询下 980 | 327 981 | 00:09:55,436 --> 00:09:57,326 982 | 然后在 RTL 的媒体查询里 983 | 328 984 | 00:09:57,326 --> 00:09:59,036 985 | 把右边距60调换成左边距 986 | 329 987 | 00:09:59,416 --> 00:10:00,276 988 | 就行了 989 | 330 990 | 00:09:59,416 --> 00:10:00,276 991 | 就行了 992 | 331 993 | 00:10:02,916 --> 00:10:05,896 994 | 现在我们再编译并运行一下 995 | 332 996 | 00:10:05,896 --> 00:10:07,076 997 | 这下播放按钮的 padding 就看起来 998 | 333 999 | 00:10:07,076 --> 00:10:07,636 1000 | 正常多了 1001 | 334 1002 | 00:10:08,146 --> 00:10:09,876 1003 | 现在页面布局完成得差不多了 1004 | 335 1005 | 00:10:09,876 --> 00:10:11,356 1006 | 但再看一下这个界面 1007 | 336 1008 | 00:10:11,356 --> 00:10:12,766 1009 | 我们发现 当横幅文字被移到 1010 | 337 1011 | 00:10:12,766 --> 00:10:13,406 1012 | 右边后 1013 | 338 1014 | 00:10:13,406 --> 00:10:14,826 1015 | 背后正好和图片内容重合 1016 | 339 1017 | 00:10:14,826 --> 00:10:16,126 1018 | 这样文字就不太容易阅读 1019 | 340 1020 | 00:10:17,166 --> 00:10:18,426 1021 | 这时网页设计师给了我 1022 | 341 1023 | 00:10:18,426 --> 00:10:19,826 1024 | 一张左右翻转的图片 1025 | 342 1026 | 00:10:19,826 --> 00:10:21,176 1027 | 可以用在从右往左布局的页面里 1028 | 343 1029 | 00:10:21,486 --> 00:10:23,006 1030 | 我就可以用图片资源目录 (Asset Catalogs) 1031 | 344 1032 | 00:10:23,006 --> 00:10:24,756 1033 | 把这张图片加到项目里 1034 | 345 1035 | 00:10:25,556 --> 00:10:26,966 1036 | 在我的这个图片资源目录里 1037 | 346 1038 | 00:10:26,966 --> 00:10:28,436 1039 | 当前只为横幅指定了 1040 | 347 1041 | 00:10:28,436 --> 00:10:29,906 1042 | 一张图片 1043 | 348 1044 | 00:10:29,906 --> 00:10:31,796 1045 | 我可以在 Attribute Inspector 里 1046 | 349 1047 | 00:10:31,796 --> 00:10:33,416 1048 | 把布局方向从 fixed (固定) 改为 1049 | 350 1050 | 00:10:33,626 --> 00:10:34,106 1051 | both (双向) 1052 | 351 1053 | 00:10:34,846 --> 00:10:36,346 1054 | 这样设置后 就会出现预留空位 1055 | 352 1056 | 00:10:36,346 --> 00:10:37,576 1057 | 把需要在从右往左布局中使用的 1058 | 353 1059 | 00:10:37,576 --> 00:10:38,696 1060 | 左右翻转的图片 拖拽到空位上 1061 | 354 1062 | 00:10:38,696 --> 00:10:39,146 1063 | 就可以了 1064 | 355 1065 | 00:10:40,656 --> 00:10:44,366 1066 | 现在我就拖拽过来 1067 | 356 1068 | 00:10:44,596 --> 00:10:46,036 1069 | 最后再编译并运行一下我的应用程序 1070 | 357 1071 | 00:10:50,436 --> 00:10:51,306 1072 | 这就是调试结果了 1073 | 358 1074 | 00:10:51,586 --> 00:10:52,996 1075 | 只需进行一些很小的改动 1076 | 359 1077 | 00:10:52,996 --> 00:10:54,536 1078 | 我们的应用程序就可以完美支持 1079 | 360 1080 | 00:10:54,536 --> 00:10:55,516 1081 | 从右往左布局了 1082 | 361 1083 | 00:10:55,516 --> 00:11:01,396 1084 | [掌声] 1085 | 362 1086 | 00:10:55,516 --> 00:11:01,396 1087 | [掌声] 1088 | 363 1089 | 00:11:01,896 --> 00:11:04,076 1090 | 总结一下 就像大家所见 1091 | 364 1092 | 00:11:04,076 --> 00:11:05,276 1093 | 只需对程序进行一些很小的改动 1094 | 365 1095 | 00:11:05,586 --> 00:11:06,866 1096 | 大部分工作都可以由默认模版中 1097 | 366 1098 | 00:11:06,866 --> 00:11:08,536 1099 | 自带的免费技术支持来完成 1100 | 367 1101 | 00:11:08,536 --> 00:11:09,956 1102 | 很轻松 就能让我们的程序 1103 | 368 1104 | 00:11:09,956 --> 00:11:10,916 1105 | 支持从右往左书写文字 1106 | 369 1107 | 00:11:10,916 --> 00:11:12,606 1108 | 我们需要做的编程改动 1109 | 370 1110 | 00:11:12,606 --> 00:11:13,386 1111 | 非常少 1112 | 371 1113 | 00:11:13,926 --> 00:11:15,356 1114 | 同时 从右往左的伪码也是一个 1115 | 372 1116 | 00:11:15,356 --> 00:11:16,366 1117 | 非常强大的工具 1118 | 373 1119 | 00:11:16,366 --> 00:11:17,626 1120 | 我们可以在不懂任何 1121 | 374 1122 | 00:11:17,626 --> 00:11:19,036 1123 | 从右往左书写语言的情况下 1124 | 375 1125 | 00:11:19,036 --> 00:11:20,566 1126 | 通过伪码模拟 查看程序 1127 | 376 1128 | 00:11:20,566 --> 00:11:21,806 1129 | 在从右往左布局下的 1130 | 377 1131 | 00:11:21,806 --> 00:11:22,286 1132 | 显示效果 1133 | 378 1134 | 00:11:23,196 --> 00:11:25,116 1135 | 还有再多说一点 如果你的应用 1136 | 379 1137 | 00:11:25,116 --> 00:11:26,356 1138 | 要使用自定义试图 1139 | 380 1140 | 00:11:26,356 --> 00:11:28,746 1141 | 我们的 AutoLayout Engine 里 1142 | 381 1143 | 00:11:28,746 --> 00:11:30,336 1144 | 还有一系列约束 (constraint) 1145 | 382 1146 | 00:11:30,336 --> 00:11:32,076 1147 | 这些约束 功能非常强大 1148 | 383 1149 | 00:11:32,076 --> 00:11:34,496 1150 | 可以帮助你们优化从右往左书写 1151 | 384 1152 | 00:11:34,496 --> 00:11:35,606 1153 | 语言模式下的布局 1154 | 385 1155 | 00:11:36,366 --> 00:11:38,066 1156 | 我们还有一个基于 API 的 1157 | 386 1158 | 00:11:38,066 --> 00:11:39,346 1159 | 布局方向属性 返回的结果 1160 | 387 1161 | 00:11:39,346 --> 00:11:40,686 1162 | 可以显示运行时 1163 | 388 1164 | 00:11:40,686 --> 00:11:42,186 1165 | 你是在一个从左往右 1166 | 389 1167 | 00:11:42,186 --> 00:11:43,446 1168 | 还是从右往左的布局 1169 | 390 1170 | 00:11:43,856 --> 00:11:45,396 1171 | 在做动画帧的屏幕 1172 | 391 1173 | 00:11:45,456 --> 00:11:46,986 1174 | 自适应运算时 这个功能 1175 | 392 1176 | 00:11:46,986 --> 00:11:47,486 1177 | 会很有用 1178 | 393 1179 | 00:11:48,486 --> 00:11:50,086 1180 | 了解从右往左书写语言的布局的 1181 | 394 1182 | 00:11:50,086 --> 00:11:52,016 1183 | 更多细节 或是 1184 | 395 1185 | 00:11:52,016 --> 00:11:52,926 1186 | 想要深入了解相关信息 1187 | 396 1188 | 00:11:52,926 --> 00:11:54,316 1189 | 我推荐大家去听去年的 1190 | 397 1191 | 00:11:54,316 --> 00:11:55,436 1192 | 两场相关演讲 1193 | 398 1194 | 00:11:55,656 --> 00:11:56,826 1195 | Internationalization Best 1196 | 399 1197 | 00:11:56,826 --> 00:11:58,396 1198 | Practices 和 What's New In 1199 | 400 1200 | 00:11:58,396 --> 00:11:59,846 1201 | International User Interfaces 1202 | 401 1203 | 00:12:01,756 --> 00:12:03,756 1204 | 我迫不及待在今年秋天的时候 1205 | 402 1206 | 00:12:03,756 --> 00:12:04,706 1207 | 能看到你们的应用程序 1208 | 403 1209 | 00:12:04,706 --> 00:12:06,186 1210 | 开始支持从右往左的语言布局 1211 | 404 1212 | 00:12:06,696 --> 00:12:07,746 1213 | 下面有请 Parry 1214 | 405 1215 | 00:12:07,746 --> 00:12:08,826 1216 | 为大家介绍模版优化项目 1217 | 406 1218 | 00:12:09,026 --> 00:12:09,386 1219 | 谢谢大家 1220 | 407 1221 | 00:12:10,516 --> 00:12:12,546 1222 | [掌声] 1223 | 408 1224 | 00:12:13,046 --> 00:12:14,896 1225 | >> 谢谢 Trevor 1226 | 409 1227 | 00:12:14,896 --> 00:12:16,406 1228 | 大家好 我是 Parry 1229 | 410 1230 | 00:12:16,676 --> 00:12:17,676 1231 | 下面我将给大家介绍 1232 | 411 1233 | 00:12:17,676 --> 00:12:18,996 1234 | 我们对模版进行的一些改进 1235 | 412 1236 | 00:12:18,996 --> 00:12:20,496 1237 | 这些改进将会提高你们 1238 | 413 1239 | 00:12:20,496 --> 00:12:21,656 1240 | 应用程序的运行性能 1241 | 414 1242 | 00:12:21,696 --> 00:12:23,346 1243 | 既可以优化响应时间 1244 | 415 1245 | 00:12:23,466 --> 00:12:23,936 1246 | 又可以减少内存占用 1247 | 416 1248 | 00:12:24,836 --> 00:12:26,426 1249 | 如果你用 TVMLKit 做过应用程序 1250 | 417 1251 | 00:12:26,426 --> 00:12:28,166 1252 | 那么也许已经注意到 1253 | 418 1254 | 00:12:28,166 --> 00:12:29,656 1255 | 当我们试图往模版里 1256 | 419 1257 | 00:12:29,656 --> 00:12:31,676 1258 | 添加更多内容的时候 1259 | 420 1260 | 00:12:31,676 --> 00:12:33,406 1261 | 模版性能会逐渐降低 1262 | 421 1263 | 00:12:33,546 --> 00:12:34,396 1264 | 让我用一个例子给大家演示 1265 | 422 1266 | 00:12:34,396 --> 00:12:34,816 1267 | 一下 1268 | 423 1269 | 00:12:36,076 --> 00:12:37,756 1270 | 我和 Trevor 一起 1271 | 424 1272 | 00:12:37,756 --> 00:12:40,246 1273 | 在开发这个 WWDC 的样本程序 1274 | 425 1275 | 00:12:40,246 --> 00:12:43,066 1276 | 我希望这个应用程序能搭载 1277 | 426 1278 | 00:12:43,066 --> 00:12:44,156 1279 | 所有的往期演讲 1280 | 427 1281 | 00:12:44,156 --> 00:12:45,236 1282 | 而不是只有去年的 1283 | 428 1284 | 00:12:45,236 --> 00:12:45,586 1285 | 演讲 1286 | 429 1287 | 00:12:46,016 --> 00:12:47,506 1288 | 我们依然维持简洁的页面设计 1289 | 430 1290 | 00:12:48,236 --> 00:12:50,036 1291 | 用网格 (grid) 来展示所有 1292 | 431 1293 | 00:12:50,036 --> 00:12:50,606 1294 | 演讲 1295 | 432 1296 | 00:12:51,096 --> 00:12:52,606 1297 | 每个演讲都由一个 lockup 元素来代表 1298 | 433 1299 | 00:12:52,606 --> 00:12:55,686 1300 | 其中包括一张图片 1301 | 434 1302 | 00:12:55,906 --> 00:12:56,426 1303 | 和一个文字标题 1304 | 435 1305 | 00:12:56,426 --> 00:12:58,716 1306 | 大家可以想象 要展示的全部内容 1307 | 436 1308 | 00:12:58,716 --> 00:12:59,806 1309 | 会包括上千个像这样的 1310 | 437 1311 | 00:12:59,806 --> 00:13:00,506 1312 | 视频 1313 | 438 1314 | 00:12:59,806 --> 00:13:00,506 1315 | 视频 1316 | 439 1317 | 00:13:01,126 --> 00:13:02,426 1318 | 所以一般针对这种典型情况 1319 | 440 1320 | 00:13:02,426 --> 00:13:03,986 1321 | 我们不希望这个页面 一上来 1322 | 441 1323 | 00:13:03,986 --> 00:13:05,596 1324 | 就显示出所有内容 1325 | 442 1326 | 00:13:05,596 --> 00:13:07,026 1327 | 因为首先 一下全部显示出来 1328 | 443 1329 | 00:13:07,026 --> 00:13:09,196 1330 | 加载时间太长 而第二 1331 | 444 1332 | 00:13:09,196 --> 00:13:10,606 1333 | 很有可能根本无法一下全部显示 1334 | 445 1335 | 00:13:10,606 --> 00:13:11,876 1336 | 那还要看你的服务器是否能够 1337 | 446 1338 | 00:13:11,876 --> 00:13:12,316 1339 | 支持 1340 | 447 1341 | 00:13:13,066 --> 00:13:14,846 1342 | 所以我们就需要给数据进行分页 (paginate) 1343 | 448 1344 | 00:13:14,846 --> 00:13:15,396 1345 | 处理 1346 | 449 1347 | 00:13:15,856 --> 00:13:17,036 1348 | 可以先显示一小部分 1349 | 450 1350 | 00:13:17,036 --> 00:13:18,856 1351 | 比方说 500 个项目 1352 | 451 1353 | 00:13:18,856 --> 00:13:20,626 1354 | 然后等用户滚动到 1355 | 452 1356 | 00:13:20,626 --> 00:13:22,516 1357 | 接近内容末尾时 1358 | 453 1359 | 00:13:22,516 --> 00:13:23,826 1360 | 再不断加载下一部分内容 1361 | 454 1362 | 00:13:25,056 --> 00:13:26,916 1363 | 我们针对这种情形做了 1364 | 455 1365 | 00:13:26,916 --> 00:13:28,586 1366 | 一个性能分析 1367 | 456 1368 | 00:13:28,946 --> 00:13:30,076 1369 | 结果差不多如图 1370 | 457 1371 | 00:13:31,636 --> 00:13:33,766 1372 | 这张图上显示的是 1373 | 458 1374 | 00:13:33,766 --> 00:13:35,866 1375 | 编译一个含有一定数量 1376 | 459 1377 | 00:13:35,866 --> 00:13:36,996 1378 | 项目的模版 需要多长时间 1379 | 460 1380 | 00:13:36,996 --> 00:13:40,786 1381 | 我们可以看到 这张图里 1382 | 461 1383 | 00:13:40,786 --> 00:13:42,056 1384 | 位于 X 轴上的数值 1385 | 462 1386 | 00:13:42,056 --> 00:13:43,736 1387 | 代表着模版中项目的数量 1388 | 463 1389 | 00:13:43,736 --> 00:13:45,126 1390 | 而 Y 轴数值 代表显示出这么多项目 1391 | 464 1392 | 00:13:45,126 --> 00:13:45,796 1393 | 所需要的时间 1394 | 465 1395 | 00:13:46,506 --> 00:13:48,346 1396 | 我们从图中可以看出 1397 | 466 1398 | 00:13:48,346 --> 00:13:49,426 1399 | 花费时间呈指数增长 1400 | 467 1401 | 00:13:50,596 --> 00:13:51,926 1402 | 也就是说 随着模版 1403 | 468 1404 | 00:13:51,926 --> 00:13:53,586 1405 | 体量增大 要加载同样多 1406 | 469 1407 | 00:13:53,586 --> 00:13:55,526 1408 | 数量的项目 就需要花费 1409 | 470 1410 | 00:13:55,526 --> 00:13:56,306 1411 | 更多的时间 1412 | 471 1413 | 00:13:56,306 --> 00:13:58,726 1414 | 这又是为什么呢 1415 | 472 1416 | 00:13:59,476 --> 00:14:01,236 1417 | 一个很重要的影响因素 1418 | 473 1419 | 00:13:59,476 --> 00:14:01,236 1420 | 一个很重要的影响因素 1421 | 474 1422 | 00:14:01,496 --> 00:14:02,876 1423 | 就是这些模版里 1424 | 475 1425 | 00:14:02,876 --> 00:14:04,426 1426 | 文档对象模型 (DOM) 的 1427 | 476 1428 | 00:14:04,456 --> 00:14:04,936 1429 | 大小 1430 | 477 1431 | 00:14:05,416 --> 00:14:06,786 1432 | 这个问题有两方面 1433 | 478 1434 | 00:14:07,686 --> 00:14:08,946 1435 | 一方面是 1436 | 479 1437 | 00:14:08,946 --> 00:14:09,786 1438 | 存在多余的模板语法解析 1439 | 480 1440 | 00:14:10,086 --> 00:14:11,816 1441 | 首先 是在你的 JavaScript 里 1442 | 481 1443 | 00:14:11,816 --> 00:14:13,076 1444 | 要把数据解析到 DOM 树里 1445 | 482 1446 | 00:14:13,076 --> 00:14:15,016 1447 | 这样 当 DOM 树越来越大 1448 | 483 1449 | 00:14:15,016 --> 00:14:16,376 1450 | 要添加新的东西进去 1451 | 484 1452 | 00:14:16,376 --> 00:14:18,356 1453 | 就需要花更多的时间 1454 | 485 1455 | 00:14:18,356 --> 00:14:20,486 1456 | 然后 TVMLKit 还得解析 DOM 1457 | 486 1458 | 00:14:20,486 --> 00:14:21,616 1459 | 来计算出显示布局 1460 | 487 1461 | 00:14:21,616 --> 00:14:24,156 1462 | 所需信息 比如内容格大小 1463 | 488 1464 | 00:14:24,266 --> 00:14:26,156 1465 | 行距 甚至滚动 1466 | 489 1467 | 00:14:26,156 --> 00:14:26,616 1468 | 偏移量 1469 | 490 1470 | 00:14:27,106 --> 00:14:28,316 1471 | 上述这些参数 会让你们的 1472 | 491 1473 | 00:14:28,316 --> 00:14:29,976 1474 | TVMLKit 程序界面看起来非常漂亮 1475 | 492 1476 | 00:14:30,966 --> 00:14:32,096 1477 | 但可以想象的是 1478 | 493 1479 | 00:14:32,096 --> 00:14:33,966 1480 | DOM 树越大 完成这些 1481 | 494 1482 | 00:14:33,966 --> 00:14:35,346 1483 | 计算所需的时间 1484 | 495 1485 | 00:14:35,346 --> 00:14:35,826 1486 | 就越长 1487 | 496 1488 | 00:14:37,226 --> 00:14:39,886 1489 | 而另一方面 DOM 树越来越大 1490 | 497 1491 | 00:14:39,886 --> 00:14:41,626 1492 | 对内存的压力也会越来越大 1493 | 498 1494 | 00:14:41,626 --> 00:14:42,926 1495 | 这就会整体拖慢 1496 | 499 1497 | 00:14:42,926 --> 00:14:44,656 1498 | 你应用程序的响应 1499 | 500 1500 | 00:14:44,656 --> 00:14:44,976 1501 | 时间 1502 | 501 1503 | 00:14:47,186 --> 00:14:49,506 1504 | 为了解决响应时间和内存占用问题 1505 | 502 1506 | 00:14:49,506 --> 00:14:52,386 1507 | 在 tvOS 11 中 我们引入了一个全新的 1508 | 503 1509 | 00:14:52,386 --> 00:14:54,296 1510 | 模版定义范式 1511 | 504 1512 | 00:14:54,296 --> 00:14:56,446 1513 | 新方法采用原型 (Prototype) 1514 | 505 1515 | 00:14:56,626 --> 00:14:59,396 1516 | 加数据绑定 (Data Binding) 来构建模版 1517 | 506 1518 | 00:14:59,396 --> 00:15:00,976 1519 | 此方法可以显著降低 DOM 树大小 1520 | 507 1521 | 00:14:59,396 --> 00:15:00,976 1522 | 此方法可以显著降低 DOM 树大小 1523 | 508 1524 | 00:15:01,016 --> 00:15:02,786 1525 | 从而提高大家 1526 | 509 1527 | 00:15:02,786 --> 00:15:05,526 1528 | 应用程序的性能 除此之外 1529 | 510 1530 | 00:15:05,526 --> 00:15:07,386 1531 | 我们还增加了一些 API 1532 | 511 1533 | 00:15:07,386 --> 00:15:08,336 1534 | 来支持数据分页 1535 | 512 1536 | 00:15:09,286 --> 00:15:10,246 1537 | 下面我们详细讨论 1538 | 513 1539 | 00:15:10,246 --> 00:15:10,586 1540 | 一下 1541 | 514 1542 | 00:15:12,036 --> 00:15:13,996 1543 | 为了方便大家理解原型 1544 | 515 1545 | 00:15:13,996 --> 00:15:15,786 1546 | 到底是什么 我先给大家 1547 | 516 1548 | 00:15:15,786 --> 00:15:17,486 1549 | 讲一下 一个典型的模版 1550 | 517 1551 | 00:15:17,486 --> 00:15:18,336 1552 | 构建过程 1553 | 518 1554 | 00:15:18,916 --> 00:15:20,296 1555 | 最开始 在我们手上的 1556 | 519 1557 | 00:15:20,296 --> 00:15:22,776 1558 | 是你服务器上的数据 和一个模版 1559 | 520 1560 | 00:15:22,776 --> 00:15:23,336 1561 | 的空壳 1562 | 521 1563 | 00:15:23,336 --> 00:15:24,456 1564 | 针对我们这个程序 是个网格模版 1565 | 522 1566 | 00:15:24,456 --> 00:15:27,086 1567 | 然后根据 JavaScript 1568 | 523 1569 | 00:15:27,086 --> 00:15:27,886 1570 | 我们需要把 1571 | 524 1572 | 00:15:27,886 --> 00:15:30,936 1573 | JavaScript 里的对象都逐一 1574 | 525 1575 | 00:15:30,936 --> 00:15:32,446 1576 | 翻译成相对应的 TVML 1577 | 526 1578 | 00:15:32,446 --> 00:15:33,096 1579 | 语言 1580 | 527 1581 | 00:15:33,496 --> 00:15:35,056 1582 | 在我们这个应用里 翻成 lockup 1583 | 528 1584 | 00:15:36,086 --> 00:15:37,866 1585 | 做完这一步之后 1586 | 529 1587 | 00:15:37,866 --> 00:15:40,736 1588 | 再把这些都转化成 DOM 树 1589 | 530 1590 | 00:15:40,736 --> 00:15:42,296 1591 | 然后 TVMLKit 就可以开始 1592 | 531 1593 | 00:15:42,296 --> 00:15:43,386 1594 | 生成 UI 了 1595 | 532 1596 | 00:15:44,056 --> 00:15:46,286 1597 | 但如果我们仔细研究一下代码 1598 | 533 1599 | 00:15:46,286 --> 00:15:47,826 1600 | 就会发现 这些 lockup 之间 1601 | 534 1602 | 00:15:47,826 --> 00:15:49,016 1603 | 相似度非常高 1604 | 535 1605 | 00:15:49,016 --> 00:15:51,736 1606 | 实际上这些 lockup 之间 唯一不同的 1607 | 536 1608 | 00:15:51,826 --> 00:15:52,856 1609 | 地方 就是它们各自 1610 | 537 1611 | 00:15:52,856 --> 00:15:53,846 1612 | 从数据中解析来的值 1613 | 538 1614 | 00:15:56,796 --> 00:15:58,606 1615 | 如果我们去除这些值 1616 | 539 1617 | 00:15:59,076 --> 00:16:00,806 1618 | 剩下的就是一些 1619 | 540 1620 | 00:15:59,076 --> 00:16:00,806 1621 | 剩下的就是一些 1622 | 541 1623 | 00:16:00,876 --> 00:16:03,756 1624 | 完全一样的 lockup 1625 | 542 1626 | 00:16:03,756 --> 00:16:05,326 1627 | 其中任何一个 所包含的信息 1628 | 543 1629 | 00:16:05,326 --> 00:16:07,386 1630 | 就已经足够 TVMLKit 预先计算出 1631 | 544 1632 | 00:16:07,386 --> 00:16:08,306 1633 | 如何布局页面了 1634 | 545 1635 | 00:16:09,216 --> 00:16:10,856 1636 | 所以 其实我们并不需要 1637 | 546 1638 | 00:16:10,856 --> 00:16:12,706 1639 | 写这么多个 lockup 1640 | 547 1641 | 00:16:12,706 --> 00:16:14,146 1642 | 写一个就够了 1643 | 548 1644 | 00:16:14,466 --> 00:16:15,896 1645 | 而提炼出的这个骨架 就是一个 1646 | 549 1647 | 00:16:15,896 --> 00:16:16,586 1648 | 原型了 1649 | 550 1650 | 00:16:17,596 --> 00:16:20,306 1651 | 所以简单说 原型就是个针对 1652 | 551 1653 | 00:16:20,306 --> 00:16:22,386 1654 | 数据对象的 TVML 语言纲要 1655 | 552 1656 | 00:16:22,386 --> 00:16:24,196 1657 | TVMLKit 可利用这一纲要 1658 | 553 1659 | 00:16:24,196 --> 00:16:26,636 1660 | 预先计算布局 然后在 1661 | 554 1662 | 00:16:26,636 --> 00:16:28,116 1663 | 运行时 再和数据 1664 | 555 1665 | 00:16:28,116 --> 00:16:30,786 1666 | 进行结合 自动帮你创建 DOM 树 1667 | 556 1668 | 00:16:30,786 --> 00:16:32,266 1669 | 但只解析它所需要的那一部分 1670 | 557 1671 | 00:16:32,266 --> 00:16:34,056 1672 | 且只在需要的时候才解析 1673 | 558 1674 | 00:16:34,876 --> 00:16:36,616 1675 | 这样就可以控制 DOM 树较小 1676 | 559 1677 | 00:16:36,926 --> 00:16:38,476 1678 | 且大小独立 不随数据变多而增长 1679 | 560 1680 | 00:16:40,386 --> 00:16:42,836 1681 | 当然 如果一个模版里 1682 | 561 1683 | 00:16:42,836 --> 00:16:44,166 1684 | 只有原型 还不完整 1685 | 562 1686 | 00:16:44,166 --> 00:16:45,776 1687 | 我们还需要给模版 1688 | 563 1689 | 00:16:45,776 --> 00:16:46,546 1690 | 提供数据 1691 | 564 1692 | 00:16:47,696 --> 00:16:49,036 1693 | 但除了提供数据 1694 | 565 1695 | 00:16:49,036 --> 00:16:51,146 1696 | 我们还需要把模版 1697 | 566 1698 | 00:16:51,146 --> 00:16:52,936 1699 | 和数据关联起来 1700 | 567 1701 | 00:16:53,226 --> 00:16:54,826 1702 | 这样 TVMLKit 才能 1703 | 568 1704 | 00:16:54,936 --> 00:16:56,756 1705 | 解析你的数据 1706 | 569 1707 | 00:16:56,756 --> 00:16:58,016 1708 | 并且帮你完成模版构建 1709 | 570 1710 | 00:16:58,746 --> 00:17:01,736 1711 | 这一关联过程就要靠数据绑定 1712 | 571 1713 | 00:16:58,746 --> 00:17:01,736 1714 | 这一关联过程就要靠数据绑定 1715 | 572 1716 | 00:17:01,736 --> 00:17:02,176 1717 | 来完成 1718 | 573 1719 | 00:17:03,696 --> 00:17:05,976 1720 | 要在 tvOS 11 里 1721 | 574 1722 | 00:17:05,976 --> 00:17:07,746 1723 | 给模版绑定数据 一共有 1724 | 575 1725 | 00:17:08,056 --> 00:17:09,116 1726 | 三种方法 1727 | 576 1728 | 00:17:09,705 --> 00:17:12,546 1729 | 首先 可以给一个元素的来源 1730 | 577 1731 | 00:17:12,665 --> 00:17:13,806 1732 | 绑定属性 1733 | 578 1734 | 00:17:14,086 --> 00:17:15,425 1735 | 这个例子里 就给 1736 | 579 1737 | 00:17:15,425 --> 00:17:17,286 1738 | 图像元素的来源属性 1739 | 580 1740 | 00:17:17,665 --> 00:17:18,776 1741 | 绑定了数据中的 1742 | 581 1743 | 00:17:18,776 --> 00:17:19,646 1744 | URL 属性 1745 | 582 1746 | 00:17:20,896 --> 00:17:22,665 1747 | 我们还可以对一个元素的文字内容 1748 | 583 1749 | 00:17:22,665 --> 00:17:23,955 1750 | 进行绑定 1751 | 584 1752 | 00:17:23,955 --> 00:17:25,856 1753 | 这个例子里 就给 title 元素的 1754 | 585 1755 | 00:17:26,146 --> 00:17:28,046 1756 | 文字内容 (textContent) 绑定了 1757 | 586 1758 | 00:17:28,046 --> 00:17:29,526 1759 | 数据中的 title 属性 1760 | 587 1761 | 00:17:30,646 --> 00:17:33,106 1762 | 最后 我们还可以把一个组件 1763 | 588 1764 | 00:17:33,106 --> 00:17:35,966 1765 | 中的某些项目绑定 1766 | 589 1767 | 00:17:35,966 --> 00:17:36,346 1768 | 对象 1769 | 590 1770 | 00:17:37,416 --> 00:17:38,906 1771 | 我们可以将组件中的 1772 | 591 1773 | 00:17:38,906 --> 00:17:40,156 1774 | 试验 DOM 元素 1775 | 592 1776 | 00:17:40,156 --> 00:17:41,356 1777 | 和一系列对象进行 1778 | 593 1779 | 00:17:41,356 --> 00:17:41,986 1780 | 绑定 1781 | 594 1782 | 00:17:44,546 --> 00:17:46,786 1783 | 所以简单说 数据绑定 1784 | 595 1785 | 00:17:46,786 --> 00:17:47,886 1786 | 就是一种链接 用来关联 1787 | 596 1788 | 00:17:47,936 --> 00:17:48,656 1789 | TVML 属性和数据属性 1790 | 597 1791 | 00:17:48,926 --> 00:17:49,796 1792 | 这种关联 可以在 TVML 里 1793 | 598 1794 | 00:17:49,826 --> 00:17:50,606 1795 | 利用一个绑定属性 1796 | 599 1797 | 00:17:50,636 --> 00:17:51,086 1798 | 来直接指定 1799 | 600 1800 | 00:17:51,116 --> 00:17:51,896 1801 | 说到为 TVMLKit 提供数据 1802 | 601 1803 | 00:17:51,926 --> 00:17:52,796 1804 | 我们还可以在 JavaScript 里 1805 | 602 1806 | 00:17:52,826 --> 00:17:53,666 1807 | 用 DataItem 来实现这一点 1808 | 603 1809 | 00:17:53,696 --> 00:17:54,476 1810 | 这是我们对 DOM 元素 1811 | 604 1812 | 00:17:54,506 --> 00:17:55,286 1813 | 引入的一个新属性 1814 | 605 1815 | 00:17:55,316 --> 00:17:55,856 1816 | 我来举个例子 1817 | 606 1818 | 00:17:55,886 --> 00:17:56,516 1819 | 我们读取 JSON 1820 | 607 1821 | 00:17:56,546 --> 00:17:57,296 1822 | 将数据转换为 JavaScript 对象 1823 | 608 1824 | 00:17:57,326 --> 00:17:58,166 1825 | 然后再用 DataItem 属性 1826 | 609 1827 | 00:17:58,196 --> 00:17:58,976 1828 | 把该对象关联到组件元素上 1829 | 610 1830 | 00:18:01,456 --> 00:18:02,996 1831 | 好了 我们接下来说 1832 | 611 1833 | 00:18:02,996 --> 00:18:03,656 1834 | 分页 1835 | 612 1836 | 00:18:03,656 --> 00:18:06,046 1837 | 在 tvOS 11 里 我们引入了 1838 | 613 1839 | 00:18:06,686 --> 00:18:08,386 1840 | 一个新事件叫作 Needs More 1841 | 614 1842 | 00:18:08,386 --> 00:18:09,986 1843 | 用这个事件 我们可以很方便地 1844 | 615 1845 | 00:18:09,986 --> 00:18:10,986 1846 | 对数据进行分页 1847 | 616 1848 | 00:18:11,026 --> 00:18:13,666 1849 | 当用户滚动到 1850 | 617 1851 | 00:18:13,816 --> 00:18:15,586 1852 | 接近页面内容末尾时 1853 | 618 1854 | 00:18:15,586 --> 00:18:18,886 1855 | 就会触发这一事件 可应用范围 1856 | 619 1857 | 00:18:18,886 --> 00:18:20,676 1858 | 包括 list shelf grid 1859 | 620 1860 | 00:18:26,756 --> 00:18:28,416 1861 | 甚至 stackTemplate 1862 | 621 1863 | 00:18:29,246 --> 00:18:32,266 1864 | 用途广泛 且几乎可以应用到 1865 | 622 1866 | 00:18:32,266 --> 00:18:33,086 1867 | 任何模版上 1868 | 623 1869 | 00:18:33,356 --> 00:18:35,416 1870 | 但要注意的是 1871 | 624 1872 | 00:18:35,416 --> 00:18:37,696 1873 | 如果要在数据绑定模板中 1874 | 625 1875 | 00:18:37,696 --> 00:18:40,676 1876 | 进行分页 还需要给数据 1877 | 626 1878 | 00:18:40,676 --> 00:18:41,946 1879 | 创建可观察对象 1880 | 627 1881 | 00:18:42,766 --> 00:18:45,436 1882 | 这样 TVMLKit 才能观察监听到 1883 | 628 1884 | 00:18:45,616 --> 00:18:47,176 1885 | 你对数据做出的改动 1886 | 629 1887 | 00:18:47,176 --> 00:18:49,556 1888 | 并在发生变动后 主动更新 1889 | 630 1890 | 00:18:50,406 --> 00:18:51,916 1891 | UI 界面 1892 | 631 1893 | 00:18:52,196 --> 00:18:54,286 1894 | 让我们看一个 如何创建 1895 | 632 1896 | 00:18:54,706 --> 00:18:57,976 1897 | 这些可观察对象的例子 1898 | 633 1899 | 00:18:57,976 --> 00:19:00,006 1900 | 开头的地方都一样 1901 | 634 1902 | 00:18:57,976 --> 00:19:00,006 1903 | 开头的地方都一样 1904 | 635 1905 | 00:19:00,146 --> 00:19:04,216 1906 | 解析 JSON 成为一个 1907 | 636 1908 | 00:19:04,216 --> 00:19:08,756 1909 | JavaScript 对象 但接下来 1910 | 637 1911 | 00:19:08,756 --> 00:19:11,176 1912 | 要遍历每一个对象 1913 | 638 1914 | 00:19:11,176 --> 00:19:17,486 1915 | 把它们映射为一个 1916 | 639 1917 | 00:19:17,486 --> 00:19:19,176 1918 | DataItem 类 1919 | 640 1920 | 00:19:19,316 --> 00:19:22,406 1921 | 这一项也是 tvOS 11 1922 | 641 1923 | 00:19:22,406 --> 00:19:25,416 1924 | 里介绍过的 本身 1925 | 642 1926 | 00:19:25,416 --> 00:19:27,046 1927 | 自带观察者模式 1928 | 643 1929 | 00:19:28,656 --> 00:19:30,096 1930 | 我们创建 dataItem 时 1931 | 644 1932 | 00:19:30,096 --> 00:19:31,686 1933 | 把它设置为可选类型 1934 | 645 1935 | 00:19:31,686 --> 00:19:32,876 1936 | 这样万一你希望模版中 1937 | 646 1938 | 00:19:32,876 --> 00:19:35,166 1939 | 有多个原型 也可以 1940 | 647 1941 | 00:19:35,166 --> 00:19:38,326 1942 | 还要规定识别码 1943 | 648 1944 | 00:19:38,326 --> 00:19:39,936 1945 | TVMLKit 可以利用这个识别码 1946 | 649 1947 | 00:19:39,936 --> 00:19:41,906 1948 | 更高效地把更新推到 1949 | 650 1950 | 00:19:41,906 --> 00:19:42,516 1951 | UI 上 1952 | 651 1953 | 00:19:43,846 --> 00:19:45,566 1954 | 映射完成后 1955 | 652 1956 | 00:19:45,566 --> 00:19:47,156 1957 | 还要把这个 DataItem 类 1958 | 653 1959 | 00:19:47,156 --> 00:19:48,596 1960 | 包在另一个 sectionDataItem 里 1961 | 654 1962 | 00:19:49,426 --> 00:19:51,946 1963 | 最终才是 把这个 sectionDataItem 关联到 1964 | 655 1965 | 00:19:52,736 --> 00:19:53,366 1966 | 元素上 1967 | 656 1968 | 00:19:53,806 --> 00:19:57,546 1969 | 我们举个例子来看看如何 1970 | 657 1971 | 00:19:57,546 --> 00:19:58,516 1972 | 处理 Needs More 1973 | 658 1974 | 00:19:59,636 --> 00:20:01,226 1975 | Needs More 和其他事件 1976 | 659 1977 | 00:19:59,636 --> 00:20:01,226 1978 | Needs More 和其他事件 1979 | 660 1980 | 00:20:01,226 --> 00:20:01,976 1981 | 一样 1982 | 661 1983 | 00:20:02,046 --> 00:20:03,806 1984 | 我们可以用 1985 | 662 1986 | 00:20:03,806 --> 00:20:05,776 1987 | Add Event Listener 方法 1988 | 663 1989 | 00:20:05,776 --> 00:20:07,846 1990 | 来监听一个 DOM 元素 1991 | 664 1992 | 00:20:07,846 --> 00:20:10,516 1993 | 再提供一个函数 1994 | 665 1995 | 00:20:10,516 --> 00:20:11,976 1996 | 这个函数执行起来 1997 | 666 1998 | 00:20:11,976 --> 00:20:13,576 1999 | 和新构建一个模板 2000 | 667 2001 | 00:20:13,576 --> 00:20:13,986 2002 | 差不多 2003 | 668 2004 | 00:20:14,176 --> 00:20:16,216 2005 | 从数据库提取数据 2006 | 669 2007 | 00:20:16,216 --> 00:20:18,296 2008 | 映射为 DataItem 类 2009 | 670 2010 | 00:20:18,296 --> 00:20:21,376 2011 | 但最后一步 2012 | 671 2013 | 00:20:21,376 --> 00:20:23,066 2014 | 不是把这些 DataItem 2015 | 672 2016 | 00:20:23,066 --> 00:20:24,156 2017 | 直接关联到元素上 2018 | 673 2019 | 00:20:24,156 --> 00:20:26,006 2020 | 而是插入到已有元素的末尾 2021 | 674 2022 | 00:20:26,746 --> 00:20:28,266 2023 | 完成以上这些操作之后 2024 | And once you've done that, you 2025 | 675 2026 | 00:20:28,266 --> 00:20:30,196 2027 | 再对 DataItem 类调用 2028 | 676 2029 | 00:20:30,246 --> 00:20:32,216 2030 | Touch Property Path Method 2031 | 677 2032 | 00:20:32,216 --> 00:20:33,466 2033 | 把你数据更新推到 2034 | 678 2035 | 00:20:33,466 --> 00:20:33,956 2036 | UI 上 2037 | 679 2038 | 00:20:35,496 --> 00:20:37,036 2039 | 现在我们综合以上讲到的 2040 | 680 2041 | 00:20:37,036 --> 00:20:42,096 2042 | 用程序给大家示范一下 2043 | 681 2044 | 00:20:42,256 --> 00:20:44,166 2045 | 刚刚 Trevor 给大家演示的时候 2046 | 682 2047 | 00:20:44,166 --> 00:20:45,366 2048 | 我在他样本程序 2049 | 683 2050 | 00:20:45,366 --> 00:20:47,376 2051 | 的脚本里添加了代码 进行了 2052 | 684 2053 | 00:20:47,376 --> 00:20:47,966 2054 | 数据分页 2055 | 685 2056 | 00:20:48,556 --> 00:20:49,896 2057 | 不如就从这里开始讲吧 2058 | 686 2059 | 00:20:51,646 --> 00:20:53,836 2060 | 我们直接在代码中 2061 | 687 2062 | 00:20:53,836 --> 00:20:55,076 2063 | 找到相应的 2064 | 688 2065 | 00:20:55,076 --> 00:20:56,846 2066 | 构建 stackDocument 的部分 2067 | 689 2068 | 00:20:57,786 --> 00:20:59,816 2069 | 我要处理的事件 就关联 2070 | 690 2071 | 00:20:59,866 --> 00:21:01,536 2072 | 在 stackElement 元素 2073 | 691 2074 | 00:20:59,866 --> 00:21:01,536 2075 | 在 stackElement 元素 2076 | 692 2077 | 00:21:01,536 --> 00:21:01,986 2078 | 上 2079 | 693 2080 | 00:21:02,856 --> 00:21:04,266 2081 | 大家可以看到 2082 | 694 2083 | 00:21:04,266 --> 00:21:05,736 2084 | 这里我先抓取了下一批 2085 | 695 2086 | 00:21:05,736 --> 00:21:06,996 2087 | 想要推到模版上的数据 2088 | 696 2089 | 00:21:06,996 --> 00:21:08,906 2090 | 读取返回的 JSON 2091 | 697 2092 | 00:21:08,906 --> 00:21:10,696 2093 | 将数据转换为 2094 | 698 2095 | 00:21:10,696 --> 00:21:12,156 2096 | JavaScript 对象 2097 | 699 2098 | 00:21:12,156 --> 00:21:15,226 2099 | 再调用 populateGrid 函数 把对象 2100 | 700 2101 | 00:21:15,256 --> 00:21:15,966 2102 | 加到模版里 2103 | 701 2104 | 00:21:16,426 --> 00:21:17,666 2105 | populateGrid 这个函数是做什么的呢 2106 | 702 2107 | 00:21:18,276 --> 00:21:21,746 2108 | 目前我创建的模版 2109 | 703 2110 | 00:21:21,746 --> 00:21:23,356 2111 | 还没有使用新的原型 2112 | 704 2113 | 00:21:23,416 --> 00:21:24,666 2114 | 加数据绑定方法 2115 | 705 2116 | 00:21:24,666 --> 00:21:25,836 2117 | 所以可能大家看着代码觉得很 2118 | 706 2119 | 00:21:25,836 --> 00:21:26,386 2120 | 熟悉 2121 | 707 2122 | 00:21:26,816 --> 00:21:28,246 2123 | 大家看一眼 也许就知道这个函数 2124 | 708 2125 | 00:21:28,246 --> 00:21:29,116 2126 | 是要做什么 2127 | 709 2128 | 00:21:29,946 --> 00:21:34,586 2129 | 首先这里 我添加了一个空网格 2130 | 710 2131 | 00:21:34,766 --> 00:21:37,056 2132 | 然后遍历此批数据中的对象 2133 | 711 2134 | 00:21:37,056 --> 00:21:38,976 2135 | 把它们都用 TVML 标记语言表述 2136 | 712 2137 | 00:21:39,456 --> 00:21:40,666 2138 | 这个例子里用的是 lockup 2139 | 713 2140 | 00:21:41,456 --> 00:21:43,486 2141 | 最后把所有这些 lockup 2142 | 714 2143 | 00:21:43,486 --> 00:21:44,926 2144 | 都直接解析到 DOM 里 2145 | 715 2146 | 00:21:46,176 --> 00:21:47,386 2147 | 我稍后会用新方法 2148 | 716 2149 | 00:21:47,386 --> 00:21:49,796 2150 | 把这些转换成数据绑定模板 2151 | 717 2152 | 00:21:49,796 --> 00:21:51,506 2153 | 但在那之前 我们先运行一次 2154 | 718 2155 | 00:21:51,686 --> 00:21:52,696 2156 | 体验一下 在 TVMLKit 里 2157 | 719 2158 | 00:21:52,696 --> 00:21:53,746 2159 | 执行分页加载 2160 | 720 2161 | 00:21:53,746 --> 00:21:54,336 2162 | 是什么样 2163 | 721 2164 | 00:22:02,046 --> 00:22:03,066 2165 | 我们可以看到 刚打开时 2166 | 722 2167 | 00:22:03,096 --> 00:22:03,796 2168 | 没什么区别 2169 | 723 2170 | 00:22:04,266 --> 00:22:05,856 2171 | 但注意看 当我滚动到 2172 | 724 2173 | 00:22:05,896 --> 00:22:08,826 2174 | 接近这个网格末尾的时候 2175 | 725 2176 | 00:22:08,826 --> 00:22:10,086 2177 | 右侧的索引条 2178 | 726 2179 | 00:22:10,086 --> 00:22:12,506 2180 | 会向上跳回 这是因为 2181 | 727 2182 | 00:22:12,506 --> 00:22:14,146 2183 | 滚动接近末尾时 程序会自动 2184 | 728 2185 | 00:22:14,146 --> 00:22:15,276 2186 | 在网格末尾插入新的项目 2187 | 729 2188 | 00:22:16,016 --> 00:22:17,706 2189 | 好 现在分页实现了 2190 | 730 2191 | 00:22:18,646 --> 00:22:20,596 2192 | 让我们回过头 做完这个例子 2193 | 731 2194 | 00:22:20,596 --> 00:22:22,866 2195 | 再用新引入的原型加 2196 | 732 2197 | 00:22:22,866 --> 00:22:24,646 2198 | 数据绑定的方法 改写优化一下 2199 | 733 2200 | 00:22:24,646 --> 00:22:25,256 2201 | 这个模版 2202 | 734 2203 | 00:22:29,456 --> 00:22:30,946 2204 | 跟大家想的差不多 2205 | 735 2206 | 00:22:31,066 --> 00:22:32,346 2207 | 要把现有模版 2208 | 736 2209 | 00:22:32,346 --> 00:22:34,706 2210 | 转换成数据绑定模板 2211 | 737 2212 | 00:22:34,706 --> 00:22:36,346 2213 | 我所需要做的全部改动 2214 | 738 2215 | 00:22:36,346 --> 00:22:38,486 2216 | 都在 populateGrid 这一个函数里 2217 | 739 2218 | 00:22:39,156 --> 00:22:40,626 2219 | 我首先要做的就是 2220 | 740 2221 | 00:22:40,626 --> 00:22:44,566 2222 | 对比原来单纯地添加空网格 2223 | 741 2224 | 00:22:44,566 --> 00:22:47,336 2225 | 我需要添加一些原型 2226 | 742 2227 | 00:22:48,076 --> 00:22:49,936 2228 | 和一个绑定网格 就像这样 2229 | 743 2230 | 00:22:54,196 --> 00:22:56,076 2231 | 接下来的一步 2232 | 744 2233 | 00:22:56,076 --> 00:22:57,926 2234 | 我不需要逐一把这些对象 2235 | 745 2236 | 00:22:57,926 --> 00:23:00,436 2237 | 都映射到标记语言 2238 | 746 2239 | 00:22:57,926 --> 00:23:00,436 2240 | 都映射到标记语言 2241 | 747 2242 | 00:23:02,206 --> 00:23:04,216 2243 | 而是需要用 DataItem 类把它们 2244 | 748 2245 | 00:23:04,216 --> 00:23:05,746 2246 | 映射为可观察对象 2247 | 749 2248 | 00:23:06,326 --> 00:23:12,056 2249 | 像这样 2250 | 750 2251 | 00:23:12,056 --> 00:23:15,796 2252 | 最后一步 我也不需要 2253 | 751 2254 | 00:23:15,796 --> 00:23:19,136 2255 | 把这些标记语言都直接 2256 | 752 2257 | 00:23:19,136 --> 00:23:21,636 2258 | 读到 DOM 里 我只需把 2259 | 753 2260 | 00:23:21,636 --> 00:23:23,606 2261 | 新创建的 DataItem 插入到 2262 | 754 2263 | 00:23:23,606 --> 00:23:26,726 2264 | 已有 DataItem 的末尾 2265 | 755 2266 | 00:23:26,816 --> 00:23:32,486 2267 | 像这样 现在我们已经完成了 2268 | 756 2269 | 00:23:32,486 --> 00:23:34,166 2270 | 所有的优化改写 再运行一下 2271 | 757 2272 | 00:23:34,226 --> 00:23:34,976 2273 | 看看效果 2274 | 758 2275 | 00:23:43,556 --> 00:23:45,426 2276 | 应用程序一启动 2277 | 759 2278 | 00:23:45,426 --> 00:23:46,416 2279 | 我们就可以注意到 2280 | 760 2281 | 00:23:46,416 --> 00:23:47,936 2282 | 加载速度更快了 尽管 2283 | 761 2284 | 00:23:47,936 --> 00:23:49,746 2285 | 启动时只加载了 2286 | 762 2287 | 00:23:49,746 --> 00:23:51,506 2288 | 很少一部分内容 2289 | 763 2290 | 00:23:51,536 --> 00:23:52,696 2291 | 也能体会到速度增快 2292 | 764 2293 | 00:23:53,086 --> 00:23:54,446 2294 | 而当我向下滚动时 2295 | 765 2296 | 00:23:54,446 --> 00:23:57,316 2297 | 可看到程序性能 2298 | 766 2299 | 00:23:57,316 --> 00:24:00,056 2300 | 表现完美 滚动效果流畅 2301 | 767 2302 | 00:23:57,316 --> 00:24:00,056 2303 | 表现完美 滚动效果流畅 2304 | 768 2305 | 00:24:00,056 --> 00:24:00,496 2306 | 顺滑 2307 | 769 2308 | 00:24:01,296 --> 00:24:03,006 2309 | 滚起来还挺上瘾的 2310 | 770 2311 | 00:24:03,006 --> 00:24:04,106 2312 | 我能玩一天 2313 | 771 2314 | 00:24:06,516 --> 00:24:12,016 2315 | [掌声] 2316 | 772 2317 | 00:24:12,516 --> 00:24:14,766 2318 | 让我们回顾一下 2319 | 773 2320 | 00:24:14,796 --> 00:24:15,356 2321 | 讲过的内容 2322 | 774 2323 | 00:24:18,656 --> 00:24:20,446 2324 | 我们刚刚讲了 如何运用原型 2325 | 775 2326 | 00:24:20,446 --> 00:24:22,456 2327 | 和数据绑定这一更优化的 2328 | 776 2329 | 00:24:22,716 --> 00:24:25,046 2330 | 模版定义范式来构建你的 2331 | 777 2332 | 00:24:25,046 --> 00:24:25,486 2333 | 模版 2334 | 778 2335 | 00:24:26,496 --> 00:24:27,926 2336 | 优化后可以降低 DOM 树 2337 | 779 2338 | 00:24:27,926 --> 00:24:29,076 2339 | 大小 提高应用程序 2340 | 780 2341 | 00:24:29,776 --> 00:24:32,106 2342 | 性能 我们还讲了如何利用 2343 | 781 2344 | 00:24:32,106 --> 00:24:33,766 2345 | Needs More 事件 便捷地 2346 | 782 2347 | 00:24:33,766 --> 00:24:34,506 2348 | 对数据进行分页 2349 | 783 2350 | 00:24:35,826 --> 00:24:37,166 2351 | 在我结束前 还想给大家 2352 | 784 2353 | 00:24:37,166 --> 00:24:38,986 2354 | 重现一个测试结果 2355 | 785 2356 | 00:24:40,386 --> 00:24:41,516 2357 | 还记得我之前给大家 2358 | 786 2359 | 00:24:41,516 --> 00:24:43,086 2360 | 展示的这张图吗 显示了 2361 | 787 2362 | 00:24:43,086 --> 00:24:44,276 2363 | 构建一个含有一定数量 2364 | 788 2365 | 00:24:44,276 --> 00:24:45,536 2366 | 项目的模版 需要多长时间 2367 | 789 2368 | 00:24:46,946 --> 00:24:48,606 2369 | 我们如例子中演示的 用原型 2370 | 790 2371 | 00:24:48,606 --> 00:24:50,626 2372 | 加数据绑定的新方法 重新构建了模版 2373 | 791 2374 | 00:24:50,626 --> 00:24:52,676 2375 | 之后重做了一次性能分析 2376 | 792 2377 | 00:24:53,536 --> 00:24:55,956 2378 | 结果显示 采用新方法后 2379 | 793 2380 | 00:24:55,956 --> 00:24:58,406 2381 | 编译同样的模版所需的时间 2382 | 794 2383 | 00:24:58,406 --> 00:24:59,426 2384 | 比起原先 缩短了 2385 | 795 2386 | 00:24:59,486 --> 00:25:00,036 2387 | 超过50% 2388 | 796 2389 | 00:24:59,486 --> 00:25:00,036 2390 | 超过50% 2391 | 797 2392 | 00:25:00,486 --> 00:25:01,816 2393 | 这个结果让我们很满意 2394 | 798 2395 | 00:25:02,716 --> 00:25:04,706 2396 | 所以我鼓励大家都去看一下 2397 | 799 2398 | 00:25:04,706 --> 00:25:06,446 2399 | 这些 API 并且在你们的应用程序中 2400 | 800 2401 | 00:25:06,446 --> 00:25:08,596 2402 | 试验一下 看看你能从中 2403 | 801 2404 | 00:25:08,596 --> 00:25:09,296 2405 | 收获什么 2406 | 802 2407 | 00:25:09,356 --> 00:25:10,986 2408 | 我就说到这里 下面由 2409 | 803 2410 | 00:25:10,986 --> 00:25:12,396 2411 | Jeremy 来给大家讲 2412 | 804 2413 | 00:25:12,666 --> 00:25:13,316 2414 | Web 审查器 (Web Inspector) 2415 | 805 2416 | 00:25:13,436 --> 00:25:13,806 2417 | 谢谢大家 2418 | 806 2419 | 00:25:14,516 --> 00:25:20,546 2420 | [掌声] 2421 | 807 2422 | 00:25:21,046 --> 00:25:21,666 2423 | >> 谢谢 Parry 2424 | 808 2425 | 00:25:21,666 --> 00:25:23,366 2426 | 大家好 我的名字是 Jeremy 2427 | 809 2428 | 00:25:23,366 --> 00:25:24,866 2429 | 今天我想和大家讲讲 2430 | 810 2431 | 00:25:24,866 --> 00:25:26,276 2432 | 在 Web 审查器 (Web Inspector) 帮助下 2433 | 811 2434 | 00:25:26,406 --> 00:25:28,336 2435 | 进行 TVMLKit 开发时 2436 | 812 2437 | 00:25:28,336 --> 00:25:30,676 2438 | 如何在开发中获得幸福 2439 | 813 2440 | 00:25:31,976 --> 00:25:34,306 2441 | 我们已经看过 Parry 和 Trevor 2442 | 814 2443 | 00:25:34,306 --> 00:25:36,126 2444 | 尝试开发 WWDC 样本程序 2445 | 815 2446 | 00:25:36,126 --> 00:25:38,066 2447 | 的精彩示范 2448 | 816 2449 | 00:25:38,066 --> 00:25:39,986 2450 | 程序开发到这一步 已经 2451 | 817 2452 | 00:25:39,986 --> 00:25:40,786 2453 | 比较能干了 2454 | 818 2455 | 00:25:41,526 --> 00:25:43,236 2456 | 目前的程序可以支持 RTL 因此 2457 | 819 2458 | 00:25:43,236 --> 00:25:44,466 2459 | 可以一键本地化 设置成 2460 | 820 2461 | 00:25:44,466 --> 00:25:46,396 2462 | 从右往左书写语言模式 2463 | 821 2464 | 00:25:46,396 --> 00:25:48,176 2465 | 构建模版时还使用了数据绑定 2466 | 822 2467 | 00:25:48,176 --> 00:25:49,266 2468 | 加原型方法 所以可以流畅地 2469 | 823 2470 | 00:25:49,266 --> 00:25:50,976 2471 | 滚动 不会卡住等待加载 2472 | 824 2473 | 00:25:52,656 --> 00:25:56,406 2474 | 但好巧不巧 就在 2475 | 825 2476 | 00:25:56,406 --> 00:25:57,456 2477 | 我们准备上线的前几天 2478 | 826 2479 | 00:25:57,456 --> 00:25:59,406 2480 | 设计师突然跑过来 2481 | 827 2482 | 00:25:59,406 --> 00:26:00,926 2483 | 给了我们这么一个版面设计 2484 | 828 2485 | 00:25:59,406 --> 00:26:00,926 2486 | 给了我们这么一个版面设计 2487 | 829 2488 | 00:26:01,296 --> 00:26:02,466 2489 | 他竖起大拇指 跟你说 2490 | 830 2491 | 00:26:02,466 --> 00:26:03,916 2492 | 这个调整一下 应该很简单 2493 | 831 2494 | 00:26:05,096 --> 00:26:06,716 2495 | 我们都知道实际调整起来 没那么 2496 | 832 2497 | 00:26:06,716 --> 00:26:07,206 2498 | 简单 2499 | 833 2500 | 00:26:07,616 --> 00:26:08,946 2501 | 对此我可以说非常感同身受 2502 | 834 2503 | 00:26:08,946 --> 00:26:10,516 2504 | 你们心里的那些抱怨声 2505 | 835 2506 | 00:26:10,516 --> 00:26:12,056 2507 | 我也都能听见 2508 | 836 2509 | 00:26:13,016 --> 00:26:14,516 2510 | 让我们一起看一下开发周期 2511 | 837 2512 | 00:26:14,516 --> 00:26:16,336 2513 | 看看尤其针对这种 UI 调整 2514 | 838 2515 | 00:26:16,336 --> 00:26:17,346 2516 | 要做哪些具体步骤 2517 | 839 2518 | 00:26:17,346 --> 00:26:18,576 2519 | 借此试图细数一下 2520 | 840 2521 | 00:26:18,576 --> 00:26:19,786 2522 | 到底这哪些点非常恼人 2523 | 841 2524 | 00:26:20,216 --> 00:26:21,916 2525 | 这个 UI 调整 首先要做的 2526 | 842 2527 | 00:26:21,916 --> 00:26:24,066 2528 | 就是预估出来所有需要调整的 2529 | 843 2530 | 00:26:24,066 --> 00:26:26,176 2531 | 部分 还要猜测 为实现目标 UI 2532 | 844 2533 | 00:26:26,176 --> 00:26:27,996 2534 | 具体需要做哪些 2535 | 845 2536 | 00:26:27,996 --> 00:26:28,236 2537 | 改动 2538 | 846 2539 | 00:26:28,826 --> 00:26:30,016 2540 | margin 是往右还是往左 2541 | 847 2542 | 00:26:30,016 --> 00:26:31,036 2543 | 移动一像素 2544 | 848 2545 | 00:26:31,036 --> 00:26:33,346 2546 | 等你预估完所有需要 2547 | 849 2548 | 00:26:33,346 --> 00:26:35,106 2549 | 调整的部位 还要 2550 | 850 2551 | 00:26:35,106 --> 00:26:36,436 2552 | 再反复编译并运行 2553 | 851 2554 | 00:26:36,436 --> 00:26:37,156 2555 | 来调试 2556 | 852 2557 | 00:26:37,326 --> 00:26:38,626 2558 | 我们需要在 Xcode 里编译并运行 2559 | 853 2560 | 00:26:38,786 --> 00:26:40,656 2561 | 等着应用启动 看一眼 2562 | 854 2563 | 00:26:40,656 --> 00:26:42,036 2564 | 改动后的效果 对不对 2565 | 855 2566 | 00:26:42,326 --> 00:26:43,576 2567 | 如果不对 还得 2568 | 856 2569 | 00:26:43,576 --> 00:26:45,136 2570 | 再一遍遍地重复这个过程 2571 | 857 2572 | 00:26:45,136 --> 00:26:45,926 2573 | 直到结果正确 2574 | 858 2575 | 00:26:47,166 --> 00:26:48,956 2576 | 然后因为这个冗长的过程 2577 | 859 2578 | 00:26:48,956 --> 00:26:51,076 2579 | 中间会丢失大量上下文 2580 | 860 2581 | 00:26:51,346 --> 00:26:52,866 2582 | 开发者很容易就会沉浸在细微末节里 2583 | 861 2584 | 00:26:52,866 --> 00:26:53,626 2585 | 而忘记最终想要的目标效果 2586 | 862 2587 | 00:26:53,626 --> 00:26:55,016 2588 | 因为在调试过程中 2589 | 863 2590 | 00:26:55,016 --> 00:26:56,506 2591 | 从你对代码做出改动 2592 | 864 2593 | 00:26:56,506 --> 00:26:57,906 2594 | 到看到屏幕上的运行效果 2595 | 865 2596 | 00:26:57,906 --> 00:26:58,546 2597 | 间隔时间实在太久 2598 | 866 2599 | 00:26:59,886 --> 00:27:02,006 2600 | 所以 如果有一样东西 2601 | 867 2602 | 00:26:59,886 --> 00:27:02,006 2603 | 所以 如果有一样东西 2604 | 868 2605 | 00:27:02,006 --> 00:27:02,966 2606 | 能帮我们解决这些问题 2607 | 869 2608 | 00:27:02,966 --> 00:27:04,806 2609 | 那该有多好 2610 | 870 2611 | 00:27:05,856 --> 00:27:07,056 2612 | 一些经验丰富的 Web 开发者 2613 | 871 2614 | 00:27:07,056 --> 00:27:08,686 2615 | 已经在使用 Web 审查器的 2616 | 872 2617 | 00:27:08,686 --> 00:27:10,206 2618 | 这些强大的功能了 2619 | 873 2620 | 00:27:10,416 --> 00:27:11,986 2621 | 包括可视化调试排错 2622 | 874 2623 | 00:27:12,486 --> 00:27:13,546 2624 | 对 LocalStorage 2625 | 875 2626 | 00:27:13,546 --> 00:27:15,056 2627 | 和 SessionStorage 的内省 甚至 2628 | 876 2629 | 00:27:15,056 --> 00:27:16,306 2630 | 对 JavaScript 进行性能分析 2631 | 877 2632 | 00:27:17,396 --> 00:27:20,156 2633 | 当前的 tvOS 在 TVMLKit 里 2634 | 878 2635 | 00:27:20,156 --> 00:27:22,596 2636 | 只支持这一小部分针对 2637 | 879 2638 | 00:27:22,596 --> 00:27:24,126 2639 | Web 审查器的功能 2640 | 880 2641 | 00:27:25,186 --> 00:27:26,746 2642 | 今天 我很高兴能宣布 2643 | 881 2644 | 00:27:26,746 --> 00:27:29,186 2645 | 在 tvOS 11 中 我们会增加 2646 | 882 2647 | 00:27:29,186 --> 00:27:30,646 2648 | 对 Web 审查器其余 2649 | 883 2650 | 00:27:30,646 --> 00:27:32,116 2651 | 几项功能的支持 2652 | 884 2653 | 00:27:33,516 --> 00:27:36,556 2654 | [掌声] 2655 | 885 2656 | 00:27:37,056 --> 00:27:38,356 2657 | >> 我们首先讲一下 2658 | 886 2659 | 00:27:38,356 --> 00:27:39,396 2660 | 可视化调试排错 (Visual Debugging) 2661 | 887 2662 | 00:27:40,646 --> 00:27:42,656 2663 | 你的 TVML 应用 是一个 2664 | 888 2665 | 00:27:42,656 --> 00:27:44,816 2666 | 以 DOM 来代表的文件 为了 2667 | 889 2668 | 00:27:44,816 --> 00:27:46,156 2669 | 可视化这些 Web 审查器 2670 | 890 2671 | 00:27:46,156 --> 00:27:47,466 2672 | 设有 Elements 标签页 2673 | 891 2674 | 00:27:47,716 --> 00:27:49,616 2675 | 这里树状显示了全部节点 2676 | 892 2677 | 00:27:49,616 --> 00:27:51,936 2678 | TVML 就用这些节点 2679 | 893 2680 | 00:27:51,936 --> 00:27:52,846 2681 | 来渲染 UI 2682 | 894 2683 | 00:27:53,536 --> 00:27:54,786 2684 | 我们首先做的一项工作 2685 | 895 2686 | 00:27:54,786 --> 00:27:56,906 2687 | 就是我们希望能完全避免 2688 | 896 2689 | 00:27:57,006 --> 00:27:58,256 2690 | 猜测 而直接定位到需要 2691 | 897 2692 | 00:27:58,256 --> 00:27:59,756 2693 | 调整的部分 要实现 2694 | 898 2695 | 00:27:59,756 --> 00:28:00,936 2696 | 这一点 就应用了 2697 | 899 2698 | 00:27:59,756 --> 00:28:00,936 2699 | 这一点 就应用了 2700 | 900 2701 | 00:28:00,936 --> 00:28:01,756 2702 | 可视化 2703 | 901 2704 | 00:28:01,806 --> 00:28:03,536 2705 | 如果将鼠标缓慢 2706 | 902 2707 | 00:28:03,536 --> 00:28:05,576 2708 | 移动到树中节点上方 2709 | 903 2710 | 00:28:05,726 --> 00:28:06,456 2711 | Web 审查器会自动 2712 | 904 2713 | 00:28:06,456 --> 00:28:07,586 2714 | 在屏幕上高亮与节点对应的 2715 | 905 2716 | 00:28:07,586 --> 00:28:08,286 2717 | UI 图形区域 2718 | 906 2719 | 00:28:08,736 --> 00:28:09,756 2720 | 甚至会提供 2721 | 907 2722 | 00:28:09,756 --> 00:28:11,756 2723 | 尺寸信息 和相关联 2724 | 908 2725 | 00:28:11,756 --> 00:28:13,116 2726 | 的都有哪些元素 2727 | 909 2728 | 00:28:14,956 --> 00:28:16,966 2729 | 现在我们准确定位了需要改动的 2730 | 910 2731 | 00:28:16,966 --> 00:28:18,326 2732 | 那个节点 就可以 2733 | 911 2734 | 00:28:18,326 --> 00:28:19,966 2735 | 针对这个节点 2736 | 912 2737 | 00:28:19,966 --> 00:28:21,836 2738 | 编辑它的 XML 2739 | 913 2740 | 00:28:22,506 --> 00:28:23,646 2741 | 期间你做出的任何改动 2742 | 914 2743 | 00:28:23,646 --> 00:28:25,616 2744 | 都会在渲染 UI 前生效 2745 | 915 2746 | 00:28:26,876 --> 00:28:27,926 2747 | 如果你不想改动 2748 | 916 2749 | 00:28:27,926 --> 00:28:29,126 2750 | XML 而只是想 2751 | 917 2752 | 00:28:29,126 --> 00:28:30,886 2753 | 重新排列屏幕上的节点 这点 2754 | 918 2755 | 00:28:30,886 --> 00:28:31,886 2756 | 也可以做到 2757 | 919 2758 | 00:28:32,196 --> 00:28:33,826 2759 | 很简单 只需拖拽节点 2760 | 920 2761 | 00:28:33,826 --> 00:28:35,206 2762 | 拖动到你期望的目标位置 2763 | 921 2764 | 00:28:35,206 --> 00:28:36,756 2765 | 屏幕界面上会自动 2766 | 922 2767 | 00:28:36,756 --> 00:28:37,756 2768 | 显示出相应的更新 2769 | 923 2770 | 00:28:39,386 --> 00:28:40,916 2771 | 也有一些开发者 想在不同 2772 | 924 2773 | 00:28:40,916 --> 00:28:42,036 2774 | 明暗度下 2775 | 925 2776 | 00:28:42,036 --> 00:28:43,746 2777 | 查看 UI 效果 亮或者暗 2778 | 926 2779 | 00:28:44,026 --> 00:28:45,376 2780 | 这点可以在模版上直接 2781 | 927 2782 | 00:28:45,376 --> 00:28:46,076 2783 | 改动属性 2784 | 928 2785 | 00:28:46,076 --> 00:28:47,336 2786 | 实际上 全部节点的 2787 | 929 2788 | 00:28:47,336 --> 00:28:49,106 2789 | 所有属性 都可以修改 2790 | 930 2791 | 00:28:49,956 --> 00:28:51,626 2792 | 最后一点 我们做出的 2793 | 931 2794 | 00:28:51,626 --> 00:28:53,246 2795 | 所有这些改动 都可以 2796 | 932 2797 | 00:28:53,246 --> 00:28:55,116 2798 | 复制出来 然后随意粘贴进 2799 | 933 2800 | 00:28:55,116 --> 00:28:56,056 2801 | 任何文件 2802 | 934 2803 | 00:28:57,386 --> 00:28:58,646 2804 | 现在我们看过了如何 2805 | 935 2806 | 00:28:58,646 --> 00:29:00,136 2807 | 改变页面布局 我们再 2808 | 936 2809 | 00:28:58,646 --> 00:29:00,136 2810 | 改变页面布局 我们再 2811 | 937 2812 | 00:29:00,136 --> 00:29:01,696 2813 | 看一下屏幕上渲染 UI 2814 | 938 2815 | 00:29:01,696 --> 00:29:02,906 2816 | 所需要用到的属性 2817 | 939 2818 | 00:29:04,046 --> 00:29:06,026 2819 | 现在 Web 审查器可让我们 2820 | 940 2821 | 00:29:06,026 --> 00:29:07,266 2822 | 查看每一个节点 2823 | 941 2824 | 00:29:07,266 --> 00:29:08,446 2825 | 所关联的对应规则 2826 | 942 2827 | 00:29:08,446 --> 00:29:09,856 2828 | 所以现在我们可以查看 2829 | 943 2830 | 00:29:09,856 --> 00:29:11,586 2831 | 渲染全部节点所用的 2832 | 944 2833 | 00:29:11,586 --> 00:29:12,826 2834 | 每一条规则 2835 | 945 2836 | 00:29:14,156 --> 00:29:15,986 2837 | 这些都按照层叠顺序 2838 | 946 2839 | 00:29:15,986 --> 00:29:17,326 2840 | 来排序 权重最大的 2841 | 947 2842 | 00:29:17,326 --> 00:29:18,916 2843 | 那些规则 显示在最上面 2844 | 948 2845 | 00:29:18,916 --> 00:29:20,136 2846 | 它们覆盖的那些规则 2847 | 949 2848 | 00:29:20,136 --> 00:29:22,586 2849 | 被排在最底端 2850 | 950 2851 | 00:29:22,726 --> 00:29:24,446 2852 | 媒体查询也可以 2853 | 951 2854 | 00:29:24,446 --> 00:29:25,736 2855 | 被可视化 所以我们也可以 2856 | 952 2857 | 00:29:25,736 --> 00:29:27,466 2858 | 查看特定媒体查询 2859 | 953 2860 | 00:29:27,536 --> 00:29:28,926 2861 | 所对应的 2862 | 954 2863 | 00:29:28,926 --> 00:29:30,596 2864 | 特定规则 举个例子 2865 | 955 2866 | 00:29:30,596 --> 00:29:32,906 2867 | 若有针对 LTR 的规则 也可以 2868 | 956 2869 | 00:29:32,906 --> 00:29:35,246 2870 | 看到相应规则被归类显示 2871 | 957 2872 | 00:29:35,246 --> 00:29:35,446 2873 | 了 2874 | 958 2875 | 00:29:35,446 --> 00:29:37,696 2876 | 最后 Web 审查器 2877 | 959 2878 | 00:29:37,696 --> 00:29:38,866 2879 | 还可以让我们查看 2880 | 960 2881 | 00:29:38,866 --> 00:29:40,336 2882 | 框架本身自带的 2883 | 961 2884 | 00:29:40,336 --> 00:29:41,376 2885 | 默认规则 2886 | 962 2887 | 00:29:41,376 --> 00:29:43,106 2888 | 这样一来 要知道做出 2889 | 963 2890 | 00:29:43,106 --> 00:29:44,286 2891 | 一个完美漂亮的 UI 成品 2892 | 964 2893 | 00:29:44,286 --> 00:29:46,126 2894 | 还需要改动哪些默认规则 2895 | 965 2896 | 00:29:46,126 --> 00:29:47,566 2897 | 也完全不需要靠开发者自己 2898 | 966 2899 | 00:29:47,656 --> 00:29:50,216 2900 | 来猜测了 2901 | 967 2902 | 00:29:50,216 --> 00:29:51,576 2903 | 当然 显示出来 2904 | 968 2905 | 00:29:51,616 --> 00:29:53,576 2906 | 这么多的规则 我们 2907 | 969 2908 | 00:29:53,956 --> 00:29:55,476 2909 | 也会提供一个合并 2910 | 970 2911 | 00:29:55,476 --> 00:29:56,506 2912 | 的版本 2913 | 971 2914 | 00:29:56,506 --> 00:29:57,886 2915 | 屏幕上渲染 UI 时 2916 | 972 2917 | 00:29:57,886 --> 00:29:59,246 2918 | 用的就是这些 2919 | 973 2920 | 00:29:59,246 --> 00:29:59,886 2921 | 样式了 2922 | 974 2923 | 00:29:59,886 --> 00:30:04,106 2924 | 在 Web 审查器里 还有 2925 | 975 2926 | 00:29:59,886 --> 00:30:04,106 2927 | 在 Web 审查器里 还有 2928 | 976 2929 | 00:30:04,106 --> 00:30:05,686 2930 | 一个重新加载 (Reload) 按钮 现在 2931 | 977 2932 | 00:30:05,686 --> 00:30:06,666 2933 | 只要点击这个按钮 2934 | 978 2935 | 00:30:06,666 --> 00:30:08,326 2936 | 就可以重启整个 2937 | 979 2938 | 00:30:08,326 --> 00:30:10,036 2939 | JavaScript 执行上下文 不需要 2940 | 980 2941 | 00:30:10,036 --> 00:30:11,336 2942 | 再通过反复编译并运行来调试 2943 | 981 2944 | 00:30:11,336 --> 00:30:11,916 2945 | 程序 2946 | 982 2947 | 00:30:12,536 --> 00:30:13,696 2948 | 讲完这些 我想用一个 2949 | 983 2950 | 00:30:13,696 --> 00:30:15,776 2951 | 演示实例 给大家展示一下 2952 | 984 2953 | 00:30:15,776 --> 00:30:16,526 2954 | 刚刚所说的功能 2955 | 985 2956 | 00:30:21,046 --> 00:30:23,496 2957 | 现在我已经事先准备 2958 | 986 2959 | 00:30:23,726 --> 00:30:25,316 2960 | 好了项目 也更新了 UI 2961 | 987 2962 | 00:30:25,316 --> 00:30:27,016 2963 | 最后要做的就是 2964 | 988 2965 | 00:30:27,016 --> 00:30:28,326 2966 | 检查程序是否可以 2967 | 989 2968 | 00:30:28,326 --> 00:30:28,856 2969 | 完美支持 RTL 2970 | 990 2971 | 00:30:28,856 --> 00:30:29,236 2972 | 好了 2973 | 991 2974 | 00:30:29,236 --> 00:30:35,126 2975 | 我们现在要再次应用 2976 | 992 2977 | 00:30:35,126 --> 00:30:36,496 2978 | 前面 Trevor 教给我们的方法 2979 | 993 2980 | 00:30:36,496 --> 00:30:37,966 2981 | 去 scheme 下 2982 | 994 2983 | 00:30:38,186 --> 00:30:39,566 2984 | 把应用程序的系统语言 2985 | 995 2986 | 00:30:39,566 --> 00:30:40,876 2987 | 改为从右往左的伪码 2988 | 996 2989 | 00:30:41,296 --> 00:30:42,976 2990 | 编译并运行一下 2991 | 997 2992 | 00:30:55,046 --> 00:30:55,216 2993 | 好了 2994 | 998 2995 | 00:30:55,216 --> 00:30:57,006 2996 | 如大家所见 整体看起来 2997 | 999 2998 | 00:30:57,076 --> 00:30:58,396 2999 | 还不错 除了页面上方的横幅 3000 | 1000 3001 | 00:30:58,396 --> 00:30:59,826 3002 | 我们还没来得及为 RTL 3003 | 1001 3004 | 00:30:59,826 --> 00:31:01,106 3005 | 做优化 3006 | 1002 3007 | 00:30:59,826 --> 00:31:01,106 3008 | 做优化 3009 | 1003 3010 | 00:31:01,726 --> 00:31:02,896 3011 | 那么接下来的调试 我们就不需要 3012 | 1004 3013 | 00:31:02,896 --> 00:31:04,276 3014 | 像 Trevor 那样 反复编译并运行 3015 | 1005 3016 | 00:31:04,276 --> 00:31:05,866 3017 | 而是试图通过 3018 | 1006 3019 | 00:31:05,866 --> 00:31:06,536 3020 | Web 审查器来做 3021 | 1007 3022 | 00:31:06,916 --> 00:31:08,136 3023 | 讲下具体操作方法 我们先 3024 | 1008 3025 | 00:31:08,136 --> 00:31:11,666 3026 | 打开 Safari 可以看到我已经 3027 | 1009 3028 | 00:31:11,666 --> 00:31:13,016 3029 | 启用了开发者工具菜单 3030 | 1010 3031 | 00:31:13,016 --> 00:31:14,576 3032 | 点进去 找到 Simulator 3033 | 1011 3034 | 00:31:14,576 --> 00:31:16,756 3035 | 然后审查你想要调试的应用 3036 | 1012 3037 | 00:31:16,756 --> 00:31:17,796 3038 | 程序 3039 | 1013 3040 | 00:31:20,266 --> 00:31:23,026 3041 | 这就打开了 Web 审查器 3042 | 1014 3043 | 00:31:23,026 --> 00:31:23,916 3044 | 可以开始调试了 3045 | 1015 3046 | 00:31:24,566 --> 00:31:26,946 3047 | 现在我们想要改动 3048 | 1016 3049 | 00:31:26,946 --> 00:31:28,116 3050 | 屏幕上的几个 UI 元素 3051 | 1017 3052 | 00:31:28,116 --> 00:31:29,506 3053 | 我们可以看到 这里的 3054 | 1018 3055 | 00:31:29,506 --> 00:31:30,646 3056 | 标题 副标题 和 3057 | 1019 3058 | 00:31:30,646 --> 00:31:31,676 3059 | 按钮 都需要调整 3060 | 1020 3061 | 00:31:31,676 --> 00:31:33,636 3062 | 用 Web 审查器 我们可以 3063 | 1021 3064 | 00:31:33,636 --> 00:31:35,236 3065 | 轻松定位到这些元素 3066 | 1022 3067 | 00:31:35,236 --> 00:31:37,006 3068 | 所对应的节点 3069 | 1023 3070 | 00:31:37,006 --> 00:31:37,976 3071 | 只要移动鼠标 就可以看到 3072 | 1024 3073 | 00:31:37,976 --> 00:31:39,076 3074 | 节点对应的元素被 3075 | 1025 3076 | 00:31:39,076 --> 00:31:39,646 3077 | 高亮了 3078 | 1026 3079 | 00:31:40,276 --> 00:31:41,626 3080 | 我们现在快速地找到标题 3081 | 1027 3082 | 00:31:41,626 --> 00:31:41,976 3083 | 来进行调整 3084 | 1028 3085 | 00:31:44,916 --> 00:31:46,706 3086 | 找到后 Web 审查器会显示出 3087 | 1029 3088 | 00:31:46,706 --> 00:31:47,876 3089 | 这个节点所对应的 3090 | 1030 3091 | 00:31:47,876 --> 00:31:49,506 3092 | 所有规则 我们现在只需 3093 | 1031 3094 | 00:31:49,506 --> 00:31:51,396 3095 | 在规则中找到这个恼人的 3096 | 1032 3097 | 00:31:51,396 --> 00:31:52,436 3098 | bottom-left 把它改为 3099 | 1033 3100 | 00:31:52,436 --> 00:31:53,186 3101 | bottom-leading 3102 | 1034 3103 | 00:31:53,516 --> 00:31:55,656 3104 | 大家注意看 我边打字 3105 | 1035 3106 | 00:31:55,656 --> 00:31:57,126 3107 | 模拟屏幕上会产生什么 3108 | 1036 3109 | 00:31:57,126 --> 00:31:57,526 3110 | 变化 3111 | 1037 3112 | 00:31:59,316 --> 00:32:01,206 3113 | 你看 屏幕显示自动更新了 3114 | 1038 3115 | 00:31:59,316 --> 00:32:01,206 3116 | 你看 屏幕显示自动更新了 3117 | 1039 3118 | 00:32:01,206 --> 00:32:03,336 3119 | 不需要我们再去编译并运行 3120 | 1040 3121 | 00:32:03,556 --> 00:32:05,946 3122 | 响应非常快 如果有人 3123 | 1041 3124 | 00:32:05,946 --> 00:32:07,376 3125 | 没看到 我们再来 3126 | 1042 3127 | 00:32:07,376 --> 00:32:08,406 3128 | 对副标题进行一下 3129 | 1043 3130 | 00:32:08,406 --> 00:32:10,256 3131 | 改动 一样的步骤 3132 | 1044 3133 | 00:32:10,546 --> 00:32:12,426 3134 | 定位到你想改动的元素 3135 | 1045 3136 | 00:32:12,426 --> 00:32:14,066 3137 | 改动相关联的样式 3138 | 1046 3139 | 00:32:16,186 --> 00:32:16,896 3140 | 好了 3141 | 1047 3142 | 00:32:17,876 --> 00:32:19,466 3143 | 我们也把播放按钮 3144 | 1048 3145 | 00:32:19,466 --> 00:32:20,536 3146 | 快速地修改一下 让它 3147 | 1049 3148 | 00:32:20,536 --> 00:32:21,676 3149 | 显示在正确的位置 3150 | 1050 3151 | 00:32:23,496 --> 00:32:24,916 3152 | 我们需要把所有的 right 3153 | 1051 3154 | 00:32:24,916 --> 00:32:26,166 3155 | 都改为 trailing 3156 | 1052 3157 | 00:32:27,866 --> 00:32:34,026 3158 | 拼一下 trailing 当然 3159 | 1053 3160 | 00:32:34,026 --> 00:32:35,526 3161 | 就像 Trevor 之前做的 为了设置 3162 | 1054 3163 | 00:32:35,526 --> 00:32:36,636 3164 | 正确的 margin 还需单独 3165 | 1055 3166 | 00:32:36,636 --> 00:32:38,676 3167 | 创建媒体查询 因为 RTL 语言模式下 3168 | 1056 3169 | 00:32:38,676 --> 00:32:40,106 3170 | 数据要写在不同位置 3171 | 1057 3172 | 00:32:40,106 --> 00:32:41,666 3173 | 所以我来复制一下 margin 3174 | 1058 3175 | 00:32:43,096 --> 00:32:43,846 3176 | 粘贴过去 3177 | 1059 3178 | 00:32:45,106 --> 00:32:46,246 3179 | 这里再改一下 3180 | 1060 3181 | 00:32:46,786 --> 00:32:47,806 3182 | 然后就好了 3183 | 1061 3184 | 00:32:48,566 --> 00:32:49,786 3185 | 我们做的这些改动 3186 | 1062 3187 | 00:32:49,786 --> 00:32:51,176 3188 | 都是在作者样式表上 3189 | 1063 3190 | 00:32:51,176 --> 00:32:52,596 3191 | 而作者样式表是包含在 3192 | 1064 3193 | 00:32:52,596 --> 00:32:54,876 3194 | 样式属性 3195 | 1065 3196 | 00:32:54,876 --> 00:32:56,376 3197 | 样式标签下 我们把这些都复制 3198 | 1066 3199 | 00:32:56,376 --> 00:32:57,016 3200 | 下来 3201 | 1067 3202 | 00:32:58,216 --> 00:32:59,456 3203 | 为了核对 我们把这些 3204 | 1068 3205 | 00:32:59,456 --> 00:33:00,786 3206 | 都粘贴到我们的 TVML 3207 | 1069 3208 | 00:32:59,456 --> 00:33:00,786 3209 | 都粘贴到我们的 TVML 3210 | 1070 3211 | 00:33:00,786 --> 00:33:01,286 3212 | 文档里 3213 | 1071 3214 | 00:33:01,796 --> 00:33:05,046 3215 | 好了 3216 | 1072 3217 | 00:33:05,126 --> 00:33:08,626 3218 | 然后这次不用编译并运行 3219 | 1073 3220 | 00:33:08,626 --> 00:33:10,966 3221 | 我们可以直接点击 3222 | 1074 3223 | 00:33:10,966 --> 00:33:11,396 3224 | 重新加载 3225 | 1075 3226 | 00:33:12,246 --> 00:33:12,896 3227 | 就好了 3228 | 1076 3229 | 00:33:12,896 --> 00:33:14,416 3230 | 一切运行正常 3231 | 1077 3232 | 00:33:14,416 --> 00:33:14,886 3233 | [掌声] 3234 | 1078 3235 | 00:33:14,886 --> 00:33:16,296 3236 | 我们回到幻灯片 3237 | 1079 3238 | 00:33:17,516 --> 00:33:19,776 3239 | [掌声] 3240 | 1080 3241 | 00:33:20,276 --> 00:33:21,496 3242 | 大家刚刚看到了 这种方法 3243 | 1081 3244 | 00:33:21,496 --> 00:33:22,926 3245 | 可以快速定位 UI 中的问题 3246 | 1082 3247 | 00:33:22,926 --> 00:33:24,286 3248 | 并对其进行调整 3249 | 1083 3250 | 00:33:24,286 --> 00:33:26,266 3251 | 应用起来的具体步骤 3252 | 1084 3253 | 00:33:26,266 --> 00:33:27,776 3254 | 是先找到具体 3255 | 1085 3256 | 00:33:27,776 --> 00:33:29,916 3257 | 受影响的节点 对它们 3258 | 1086 3259 | 00:33:29,916 --> 00:33:31,486 3260 | 进行实时编辑 最后 3261 | 1087 3262 | 00:33:31,486 --> 00:33:32,936 3263 | 只需把 TVML 属性 3264 | 1088 3265 | 00:33:33,246 --> 00:33:35,276 3266 | 复制出来并粘贴 3267 | 1089 3268 | 00:33:35,446 --> 00:33:36,706 3269 | 然后直接重启整个 JavaScript 3270 | 1090 3271 | 00:33:36,706 --> 00:33:38,706 3272 | 执行上下文即可 不再需要 3273 | 1091 3274 | 00:33:38,706 --> 00:33:39,896 3275 | 一遍遍地编译并运行 3276 | 1092 3277 | 00:33:40,946 --> 00:33:42,406 3278 | 以上这就是可视化调试排错 3279 | 1093 3280 | 00:33:42,406 --> 00:33:43,916 3281 | 我下面开始介绍 3282 | 1094 3283 | 00:33:43,916 --> 00:33:44,936 3284 | 网络分析 (Network Analysis) 3285 | 1095 3286 | 00:33:46,036 --> 00:33:47,526 3287 | Web 审查器现在支持 3288 | 1096 3289 | 00:33:47,526 --> 00:33:49,336 3290 | 查看从 TVMLKit 3291 | 1097 3292 | 00:33:49,336 --> 00:33:50,686 3293 | 应用发出的所有 3294 | 1098 3295 | 00:33:50,686 --> 00:33:51,306 3296 | 网络请求 3297 | 1099 3298 | 00:33:52,166 --> 00:33:53,846 3299 | 我们可以查看 3300 | 1100 3301 | 00:33:53,846 --> 00:33:55,026 3302 | 单项请求的耗时信息 3303 | 1101 3304 | 00:33:55,026 --> 00:33:56,996 3305 | 比如 DNS 查询花了多长时间 3306 | 1102 3307 | 00:33:56,996 --> 00:33:58,396 3308 | 比如响应传输需要花费 3309 | 1103 3310 | 00:33:58,396 --> 00:33:59,436 3311 | 多长时间 3312 | 1104 3313 | 00:34:00,206 --> 00:34:01,846 3314 | 我们还可以审查 3315 | 1105 3316 | 00:34:01,846 --> 00:34:03,246 3317 | 你给某一个请求单独 3318 | 1106 3319 | 00:34:03,246 --> 00:34:04,626 3320 | 设置的审查属性 3321 | 1107 3322 | 00:34:04,626 --> 00:34:06,336 3323 | 可用来确认发出去的信息 3324 | 1108 3325 | 00:34:06,336 --> 00:34:07,326 3326 | 符合预期 3327 | 1109 3328 | 00:34:07,326 --> 00:34:09,556 3329 | 最后 如果需要 3330 | 1110 3331 | 00:34:09,556 --> 00:34:11,206 3332 | 我们还可以查看响应头 3333 | 1111 3334 | 00:34:11,206 --> 00:34:13,996 3335 | 和请求头 3336 | 1112 3337 | 00:34:14,056 --> 00:34:15,275 3338 | 可能你目前还写了脚本 3339 | 1113 3340 | 00:34:15,275 --> 00:34:17,505 3341 | 来内省 LocalStorage 3342 | 1114 3343 | 00:34:17,505 --> 00:34:18,806 3344 | 和 SessionStorage 3345 | 1115 3346 | 00:34:19,206 --> 00:34:20,286 3347 | 以后这个工作也不需要做了 3348 | 1116 3349 | 00:34:20,286 --> 00:34:21,426 3350 | 因为 Web 审查器也会 3351 | 1117 3352 | 00:34:21,426 --> 00:34:23,126 3353 | 提供一个很好的 UI 3354 | 1118 3355 | 00:34:23,406 --> 00:34:24,696 3356 | 让我们看到我们 LocalStorage 3357 | 1119 3358 | 00:34:24,696 --> 00:34:26,815 3359 | 和 SessionStorage 里每一个 3360 | 1120 3361 | 00:34:26,815 --> 00:34:28,936 3362 | 关键值路径 3363 | 1121 3364 | 00:34:28,936 --> 00:34:30,456 3365 | 而能查看这些数据 也就意味着 3366 | 1122 3367 | 00:34:30,456 --> 00:34:31,766 3368 | 我们可以进行修改 3369 | 1123 3370 | 00:34:32,016 --> 00:34:33,775 3371 | 可以进行复制 删除 3372 | 1124 3373 | 00:34:33,775 --> 00:34:34,766 3374 | 也可以随意移动 3375 | 1125 3376 | 00:34:35,326 --> 00:34:38,516 3377 | 就这些 这就是 TVMLKit 3378 | 1126 3379 | 00:34:38,516 --> 00:34:39,985 3380 | 如何支持 Web 审查器 3381 | 1127 3382 | 00:34:39,985 --> 00:34:40,966 3383 | 这就是如何在开发中获得 3384 | 1128 3385 | 00:34:40,966 --> 00:34:41,606 3386 | 幸福 3387 | 1129 3388 | 00:34:42,516 --> 00:34:45,545 3389 | [掌声] 3390 | 1130 3391 | 00:34:46,045 --> 00:34:47,045 3392 | 总结下 我们今天讲了什么 3393 | 1131 3394 | 00:34:47,775 --> 00:34:49,666 3395 | 首先 TVMLKit 自身 对于所有默认模版 3396 | 1132 3397 | 00:34:49,666 --> 00:34:50,806 3398 | 都提供对从右往左书写语言 3399 | 1133 3400 | 00:34:50,806 --> 00:34:51,356 3401 | 的支持 3402 | 1134 3403 | 00:34:51,476 --> 00:34:52,616 3404 | 如果你的应用程序中有自定义样式 3405 | 1135 3406 | 00:34:52,616 --> 00:34:54,436 3407 | 可能配置起来会复杂一点 3408 | 1136 3409 | 00:34:54,436 --> 00:34:55,295 3410 | 但都很简单 3411 | 1137 3412 | 00:34:56,005 --> 00:34:57,326 3413 | 像 Parry 讲的 如果你想要流畅 3414 | 1138 3415 | 00:34:57,326 --> 00:34:58,286 3416 | 顺滑的滚动用户体验 3417 | 1139 3418 | 00:34:58,286 --> 00:35:00,606 3419 | 请使用数据绑定和原型 3420 | 1140 3421 | 00:34:58,286 --> 00:35:00,606 3422 | 请使用数据绑定和原型 3423 | 1141 3424 | 00:35:00,736 --> 00:35:02,906 3425 | 最后 如果你希望在 3426 | 1142 3427 | 00:35:02,906 --> 00:35:04,426 3428 | 开发应用程序时 3429 | 1143 3430 | 00:35:04,426 --> 00:35:05,876 3431 | 减少调试排错的时间 请使用 3432 | 1144 3433 | 00:35:05,876 --> 00:35:06,456 3434 | Web 审查器 3435 | 1145 3436 | 00:35:06,646 --> 00:35:08,386 3437 | 更多信息 请访问 3438 | 1146 3439 | 00:35:08,386 --> 00:35:09,456 3440 | 屏幕上显示的网址 3441 | 1147 3442 | 00:35:09,756 --> 00:35:10,656 3443 | 网站上有样本代码和 3444 | 1148 3445 | 00:35:10,656 --> 00:35:11,646 3446 | 支持文件 都非常有用 3447 | 1149 3448 | 00:35:11,646 --> 00:35:12,606 3449 | 值得一看 3450 | 1150 3451 | 00:35:13,286 --> 00:35:15,026 3452 | 另外还有一些 其他很棒的演讲可以参加 3453 | 1151 3454 | 00:35:15,026 --> 00:35:16,686 3455 | 尤其 What's New in 3456 | 1152 3457 | 00:35:16,686 --> 00:35:18,116 3458 | tvOS tomorrow 3459 | 1153 3460 | 00:35:18,956 --> 00:35:21,486 3461 | 感谢大家参加 WWDC 2017 3462 | 1154 3463 | 00:35:21,556 --> 00:35:22,456 3464 | 希望大家在大会剩下的时光 3465 | 1155 3466 | 00:35:22,456 --> 00:35:22,946 3467 | 都过得愉快 -------------------------------------------------------------------------------- /resources/Design.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingcos/WWDCHelper/c88f09856359d42155bdcb09e40283997889de12/resources/Design.sketch -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingcos/WWDCHelper/c88f09856359d42155bdcb09e40283997889de12/resources/logo.png --------------------------------------------------------------------------------