├── Images
└── preview_ios.gif
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Sources
└── SwiftUIYouTubePlayer
│ ├── Model
│ ├── YouTubePlayerStatus.swift
│ ├── YouTubePlaybackQuality.swift
│ ├── YouTubePlayerEvents.swift
│ ├── YouTubePlayerAction.swift
│ ├── YouTubePlayerParameters.swift
│ ├── YouTubePlayerState.swift
│ └── YouTubePlayerConfig.swift
│ ├── Extensions
│ └── URL+Extensions.swift
│ └── SwiftUIYouTubePlayer.swift
├── Package.resolved
├── Package.swift
├── LICENSE
├── .gitignore
└── README.md
/Images/preview_ios.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/globulus/swiftui-youtube-player/HEAD/Images/preview_ios.gif
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sources/SwiftUIYouTubePlayer/Model/YouTubePlayerStatus.swift:
--------------------------------------------------------------------------------
1 | public enum YouTubePlayerStatus: String, Equatable {
2 | case unstarted = "-1"
3 | case ended = "0"
4 | case playing = "1"
5 | case paused = "2"
6 | case buffering = "3"
7 | case queued = "4"
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/SwiftUIYouTubePlayer/Model/YouTubePlaybackQuality.swift:
--------------------------------------------------------------------------------
1 | public enum YouTubePlaybackQuality: String, Equatable {
2 | case small = "small"
3 | case medium = "medium"
4 | case large = "large"
5 | case hd720 = "hd720"
6 | case hd1080 = "hd1080"
7 | case highResolution = "highres"
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/SwiftUIYouTubePlayer/Model/YouTubePlayerEvents.swift:
--------------------------------------------------------------------------------
1 | public enum YouTubePlayerEvents: String, Equatable {
2 | case youTubeIframeAPIReady = "onYouTubeIframeAPIReady"
3 | case ready = "onReady"
4 | case statusChange = "onStateChange"
5 | case playbackQualityChange = "onPlaybackQualityChange"
6 | }
7 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "SwiftUIWebView",
6 | "repositoryURL": "https://github.com/globulus/swiftui-webview",
7 | "state": {
8 | "branch": null,
9 | "revision": "62d86f9c63f457603bec3e8bd963fae392da230a",
10 | "version": "1.0.6"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SwiftUIYouTubePlayer/Model/YouTubePlayerAction.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum YouTubePlayerAction: Equatable {
4 | case idle
5 | case loadURL(URL)
6 | case loadID(String)
7 | case loadPlaylistID(String)
8 | case mute
9 | case unmute
10 | case play
11 | case pause
12 | case stop
13 | case clear
14 | case seek(Float, Bool)
15 | case duration
16 | case currentTime
17 | case previous
18 | case next
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftUIYouTubePlayer/Model/YouTubePlayerParameters.swift:
--------------------------------------------------------------------------------
1 | //"{\n\"width\":\"100%\",\n\"height\":\"100%\",\n\"playerVars\":{\n\"playsinline\":0,\n\"controls\":0,\n\"showinfo\":0\n},\n\"events\":{\n\"onPlaybackQualityChange\":\"onPlaybackQualityChange\",\n\"onStateChange\":\"onStateChange\",\n\"onReady\":\"onReady\",\n\"onError\":\"onPlayerError\"\n},\n\"videoId\":\"dQw4w9WgXcQ\"\n}"
2 |
3 | struct YouTubePlayerParameters: Encodable {
4 | let events = Events()
5 | let height = "100%"
6 | let width = "100%"
7 |
8 | var playerVars: YouTubePlayerConfig = .default
9 | var videoId: String?
10 | }
11 |
12 | struct Events: Encodable {
13 | let onPlaybackQualityChange = "onPlaybackQualityChange"
14 | let onStateChange = "onStateChange"
15 | let onReady = "onReady"
16 | let onError = "onPlayerError"
17 | }
18 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
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: "SwiftUIYouTubePlayer",
8 | platforms: [
9 | .iOS(.v14), .macOS(.v11)
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "SwiftUIYouTubePlayer",
15 | targets: ["SwiftUIYouTubePlayer"]),
16 | ],
17 | dependencies: [
18 | .package(name: "SwiftUIWebView", url: "https://github.com/globulus/swiftui-webview", from: "1.0.5")
19 | ],
20 | targets: [
21 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
22 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
23 | .target(
24 | name: "SwiftUIYouTubePlayer",
25 | dependencies: ["SwiftUIWebView"]),
26 | ]
27 | )
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Gordan Glavaš
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 |
--------------------------------------------------------------------------------
/Sources/SwiftUIYouTubePlayer/Extensions/URL+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension URL {
4 | func queryStringComponents() -> [String: AnyObject] {
5 |
6 | var dict = [String: AnyObject]()
7 |
8 | // Check for query string
9 | if let query = self.query {
10 |
11 | // Loop through pairings (separated by &)
12 | for pair in query.components(separatedBy: "&") {
13 |
14 | // Pull key, val from from pair parts (separated by =) and set dict[key] = value
15 | let components = pair.components(separatedBy: "=")
16 | if (components.count > 1) {
17 | dict[components[0]] = components[1] as AnyObject?
18 | }
19 | }
20 |
21 | }
22 |
23 | return dict
24 | }
25 | }
26 |
27 | func videoIDFromYouTubeURL(_ videoURL: URL) -> String? {
28 | if videoURL.pathComponents.count > 1 && videoURL.host?.hasSuffix("youtu.be") == true {
29 | return videoURL.pathComponents[1]
30 | } else if videoURL.pathComponents.contains("embed") {
31 | return videoURL.pathComponents.last
32 | }
33 | return videoURL.queryStringComponents()["v"] as? String
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/SwiftUIYouTubePlayer/Model/YouTubePlayerState.swift:
--------------------------------------------------------------------------------
1 | public struct YouTubePlayerState: Equatable {
2 | public internal(set) var ready: Bool
3 | public internal(set) var status: YouTubePlayerStatus
4 | public internal(set) var quality: YouTubePlaybackQuality
5 | public internal(set) var duration: Double?
6 | public internal(set) var currentTime: Double?
7 | public internal(set) var error: Error?
8 | internal var iframeReady: Bool
9 |
10 | public static let empty = YouTubePlayerState(ready: false,
11 | status: .unstarted,
12 | quality: .small,
13 | duration: nil,
14 | currentTime: nil,
15 | error: nil,
16 | iframeReady: false)
17 |
18 | public static func == (lhs: YouTubePlayerState, rhs: YouTubePlayerState) -> Bool {
19 | lhs.ready == rhs.ready
20 | && lhs.status == rhs.status
21 | && lhs.quality == rhs.quality
22 | && lhs.duration == rhs.duration
23 | && lhs.currentTime == rhs.currentTime
24 | && lhs.error?.localizedDescription == rhs.error?.localizedDescription
25 | && lhs.iframeReady == rhs.iframeReady
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/SwiftUIYouTubePlayer/Model/YouTubePlayerConfig.swift:
--------------------------------------------------------------------------------
1 | public struct YouTubePlayerConfig: Encodable {
2 | var playInline: Bool
3 | var allowControls: Bool
4 | var showInfo: Bool
5 | var listType: String?
6 | var list: String?
7 |
8 | public init(
9 | playInline: Bool = true,
10 | allowControls: Bool = false,
11 | showInfo: Bool = true
12 | ) {
13 | self.playInline = playInline
14 | self.allowControls = allowControls
15 | self.showInfo = showInfo
16 | }
17 |
18 | public static let `default`: Self = .init(
19 | playInline: true,
20 | allowControls: false,
21 | showInfo: false
22 | )
23 |
24 |
25 | enum CodingKeys: String, CodingKey {
26 | case playInline = "playsinline"
27 | case allowControls = "controls"
28 | case showInfo = "showinfo"
29 | case listType
30 | case list
31 | }
32 |
33 | public func encode(to encoder: Encoder) throws {
34 | var container = encoder.container(keyedBy: CodingKeys.self)
35 | try container.encode(playInline ? 1 : 0, forKey: .playInline)
36 | try container.encode(allowControls ? 1 : 0, forKey: .allowControls)
37 | try container.encode(showInfo ? 1 : 0, forKey: .showInfo)
38 | try container.encode(listType, forKey: .listType)
39 | try container.encode(list, forKey: .list)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUI YouTube Player for iOS and MacOS
2 |
3 | Fully functional, SwiftUI-ready YouTube player for iOS 14+ and MacOS 11+. Actions and state are both delivered via SwiftUI `@Binding`s, meaking it dead-easy to integrate into any existing SwiftUI View.
4 |
5 | 
6 |
7 | ## Installation
8 |
9 | This component is distributed as a **Swift package**. Just add this repo's URL to XCode:
10 |
11 | ```text
12 | https://github.com/globulus/swiftui-youtube-player
13 | ```
14 |
15 | ## How to use
16 |
17 | * Pass the **config** parameter to optionally set various player properties:
18 | + `playInline`
19 | + `allowsControls`
20 | + `showInfo`
21 | * The **action** binding is used to control the player - whichever action you want it to perform, just set the variable's value to it. Available actions:
22 | + `idle` - does nothing and can be used as the default value.
23 | + `load(URLRequest)` - loads the video from the provided URL.
24 | + `loadID(String)` - loads the video based on its YouTube ID.
25 | + `loadPlaylistId(String)` - loads a playlist based on its YouTube ID.
26 | + `mute`
27 | + `unmute`
28 | + `play`
29 | + `pause`
30 | + `stop`
31 | + `clear`
32 | + `seek(Float, Bool` - seeks the given position in the video.
33 | + `duration` - evaluates the video's duration and updates the state.
34 | + `currentTime` - evaluates the current play time and updates the state.
35 | + `previous`
36 | + `next`
37 | * The **state** binding reports back the current state of the player. Available data:
38 | + `ready` - `true` if the player is ready to play a video.
39 | + `status` - `unstarted, ended, playing, paused, buffering, queued`
40 | + `quality` - `small, medium, large, hd720, hd1080, highResolution`
41 | + `duration` - will be set after the `duration` action is invoked.
42 | + `currentTime` - will be set after the `currentTime` action is invoked.
43 | + `error` - set if an error ocurred while playing the video, `nil` otherwise.
44 |
45 | ## Sample code
46 |
47 | ```swift
48 | import SwiftUIYouTubePlayer
49 |
50 | struct YouTubeTest: View {
51 | @State private var action = YouTubePlayerAction.idle
52 | @State private var state = YouTubePlayerState.empty
53 |
54 | private var buttonText: String {
55 | switch state.status {
56 | case .playing:
57 | return "Pause"
58 | case .unstarted, .ended, .paused:
59 | return "Play"
60 | case .buffering, .queued:
61 | return "Wait"
62 | }
63 | }
64 | private var infoText: String {
65 | "Q: \(state.quality)"
66 | }
67 |
68 | var body: some View {
69 | VStack {
70 | HStack {
71 | Button("Load") {
72 | action = .loadID("v1PBptSDIh8")
73 | }
74 | Button(buttonText) {
75 | if state.status != .playing {
76 | action = .play
77 | } else {
78 | action = .pause
79 | }
80 | }
81 | Text(infoText)
82 | Button("Prev") {
83 | action = .previous
84 | }
85 | Button("Next") {
86 | action = .next
87 | }
88 | }
89 | YouTubePlayer(action: $action, state: $state)
90 | Spacer()
91 | }
92 | }
93 | }
94 | ```
95 |
96 | ## Recipe
97 |
98 | For a more detailed description of the code, [visit this recipe](https://swiftuirecipes.com/blog/swiftui-play-youtube-video). Check out [SwiftUIRecipes.com](https://swiftuirecipes.com) for more **SwiftUI recipes**!
99 |
100 | ## Acknowledgements
101 |
102 | * The component internally uses [SwiftUI WebView](https://github.com/globulus/swiftui-webview) to render YouTube content.
103 | * Most functionality was inspired by the [Swift YouTube Player](https://github.com/gilesvangruisen/Swift-YouTube-Player) component.
104 |
105 | ## Changelog
106 |
107 | * 1.0.2 - Fixed player vars, code cleanup.
108 | * 1.0.1 - Update to work with SwiftUIWebView 1.0.5.
109 | * 1.0.0 - Initial release.
110 |
--------------------------------------------------------------------------------
/Sources/SwiftUIYouTubePlayer/SwiftUIYouTubePlayer.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SwiftUIWebView
3 |
4 | /** Embed and control YouTube videos */
5 | public struct YouTubePlayer: View {
6 |
7 | private static let webViewConfig = WebViewConfig(
8 | javaScriptEnabled: true,
9 | allowsBackForwardNavigationGestures: false,
10 | allowsInlineMediaPlayback: true,
11 | isScrollEnabled: false,
12 | isOpaque: true,
13 | backgroundColor: .clear
14 | )
15 |
16 | private static let playerHTML = """
17 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
49 |
50 |
51 | """
52 | private static let getDurationCommand = "getDuration()"
53 | private static let getCurrentTimeCommand = "getCurrentTime()"
54 |
55 | @Binding var action: YouTubePlayerAction
56 | @Binding var state: YouTubePlayerState
57 |
58 | @State private var webViewAction = WebViewAction.idle
59 | @State private var webViewState = WebViewState.empty
60 | @State private var playerVars: YouTubePlayerConfig
61 |
62 | public init(
63 | action: Binding,
64 | state: Binding,
65 | config: YouTubePlayerConfig = .default
66 | ) {
67 | _action = action
68 | _state = state
69 | _playerVars = State(initialValue: config)
70 | }
71 |
72 | public var body: some View {
73 | WebView(
74 | config: YouTubePlayer.webViewConfig,
75 | action: $webViewAction,
76 | state: $webViewState,
77 | schemeHandlers: ["ytplayer": handleJSEvent(_:)]
78 | )
79 | .onChange(of: action, perform: handleAction(_:))
80 | //.onChange(of: webViewState, perform: handleWebViewStateChange(_:))
81 | }
82 |
83 | private func handleAction(_ value: YouTubePlayerAction) {
84 | if value == .idle {
85 | return
86 | }
87 | switch value {
88 | case .idle:
89 | break
90 | case .loadURL(let url):
91 | if let id = videoIDFromYouTubeURL(url) {
92 | loadVideo(id: id)
93 | } else {
94 | onError(URLError(.badURL))
95 | }
96 | case .loadID(let id):
97 | loadVideo(id: id)
98 | case .loadPlaylistID(let id):
99 | // No videoId necessary when listType = playlist, list = [playlist Id]
100 | playerVars.listType = "playlist"
101 | playerVars.list = id
102 | let params = YouTubePlayerParameters(playerVars: playerVars)
103 | loadWebViewWithParameters(params)
104 | case .mute:
105 | evaluatePlayerCommand("mute()")
106 | case .unmute:
107 | evaluatePlayerCommand("unMute()")
108 | case .play:
109 | evaluatePlayerCommand("playVideo()")
110 | case .pause:
111 | evaluatePlayerCommand("pauseVideo()")
112 | case .stop:
113 | evaluatePlayerCommand("stopVideo()")
114 | case .clear:
115 | evaluatePlayerCommand("clearVideo()")
116 | case .seek(let seconds, let seekAhead):
117 | evaluatePlayerCommand("seekTo(\(seconds), \(seekAhead))")
118 | case .duration:
119 | evaluatePlayerCommand(YouTubePlayer.getDurationCommand) { result in
120 | if let value = result as? Double {
121 | var newState = state
122 | newState.duration = value
123 | state = newState
124 | }
125 | }
126 | case .currentTime:
127 | evaluatePlayerCommand(YouTubePlayer.getCurrentTimeCommand) { result in
128 | if let value = result as? Double {
129 | var newState = state
130 | newState.currentTime = value
131 | state = newState
132 | }
133 | }
134 | case .previous:
135 | evaluatePlayerCommand("previousVideo()")
136 | case .next:
137 | evaluatePlayerCommand("nextVideo()")
138 | }
139 | action = .idle
140 | }
141 |
142 | private func onError(_ error: Error) {
143 | var newState = state
144 | newState.error = error
145 | state = newState
146 | }
147 |
148 | private func loadVideo(id: String) {
149 | let params = YouTubePlayerParameters(
150 | playerVars: playerVars,
151 | videoId: id
152 | )
153 | loadWebViewWithParameters(params)
154 | }
155 |
156 | private func evaluatePlayerCommand(_ command: String, callback: ((Any?) -> Void)? = nil) {
157 | let fullCommand = "player.\(command);"
158 | webViewAction = .evaluateJS(fullCommand, { result in
159 | switch result {
160 | case .success(let value):
161 | callback?(value)
162 | case .failure(let error):
163 | if (error as NSError).code == 5 { // NOTE: ignore :Void return
164 | callback?(nil)
165 | } else {
166 | onError(error)
167 | }
168 | }
169 | })
170 | }
171 |
172 | // MARK: Player setup
173 | private func loadWebViewWithParameters(_ parameters: YouTubePlayerParameters) {
174 | let rawHTMLString = YouTubePlayer.playerHTML
175 | // Get JSON serialized parameters string
176 | let encoder = JSONEncoder()
177 | let jsonData = try! encoder.encode(parameters)
178 | let jsonStr = String(data: jsonData, encoding: .utf8)!
179 | // Replace %@ in rawHTMLString with jsonParameters string
180 | let htmlString = rawHTMLString.replacingOccurrences(of: "%@", with: jsonStr)
181 | // Load HTML in web view
182 | webViewAction = .loadHTML(htmlString)
183 | }
184 |
185 | private func serializedJSON(_ object: AnyObject) -> String? {
186 | guard let jsonData = try? JSONSerialization.data(withJSONObject: object, options: .prettyPrinted),
187 | let string = String(data: jsonData, encoding: .utf8)
188 | else {
189 | return nil
190 | }
191 | return string
192 | }
193 |
194 | // MARK: JS Event Handling
195 | private func handleJSEvent(_ eventURL: URL) {
196 | // Grab the last component of the queryString as string
197 | let data: String? = eventURL.queryStringComponents()["data"] as? String
198 | if let host = eventURL.host, let event = YouTubePlayerEvents(rawValue: host) {
199 | // Check event type and handle accordingly
200 | switch event {
201 | case .youTubeIframeAPIReady:
202 | var newState = state
203 | newState.iframeReady = true
204 | state = newState
205 | case .ready:
206 | var newState = state
207 | newState.ready = true
208 | state = newState
209 | case .statusChange:
210 | if let newStatus = YouTubePlayerStatus(rawValue: data!) {
211 | var newState = state
212 | newState.status = newStatus
213 | state = newState
214 | }
215 | case .playbackQualityChange:
216 | if let newQuality = YouTubePlaybackQuality(rawValue: data!) {
217 | var newState = state
218 | newState.quality = newQuality
219 | state = newState
220 | }
221 | }
222 | }
223 | }
224 | }
225 |
226 | // MARK: - Preview
227 |
228 | struct YouTubeTest: View {
229 | @State private var action = YouTubePlayerAction.idle
230 | @State private var state = YouTubePlayerState.empty
231 |
232 | private var buttonText: String {
233 | switch state.status {
234 | case .playing:
235 | return "Pause"
236 | case .unstarted, .ended, .paused:
237 | return "Play"
238 | case .buffering, .queued:
239 | return "Wait"
240 | }
241 | }
242 | private var infoText: String {
243 | "Q: \(state.quality)"
244 | }
245 |
246 | var body: some View {
247 | VStack {
248 | HStack {
249 | Button("Load") {
250 | action = .loadID("v1PBptSDIh8")
251 | }
252 | Button(buttonText) {
253 | if state.status != .playing {
254 | action = .play
255 | } else {
256 | action = .pause
257 | }
258 | }
259 | Text(infoText)
260 | Button("Prev") {
261 | action = .previous
262 | }
263 | Button("Next") {
264 | action = .next
265 | }
266 | }
267 | YouTubePlayer(action: $action, state: $state, config: .init(playInline: true))
268 | .aspectRatio(16/9, contentMode: .fit)
269 | Spacer()
270 | }
271 | }
272 | }
273 |
274 | struct YouTubePlayer_Previews: PreviewProvider {
275 | static var previews: some View {
276 | YouTubeTest()
277 | }
278 | }
279 |
--------------------------------------------------------------------------------