├── .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 |
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 | 
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 |
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 | 
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 = "\nAdvanced 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
--------------------------------------------------------------------------------