├── 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 | ![Preview iOS](https://github.com/globulus/swiftui-youtube-player/blob/main/Images/preview_ios.gif?raw=true) 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 | --------------------------------------------------------------------------------