Skip to content

Commit 1fcf840

Browse files
committed
Add HTMLBuilder and JavaScript evaluation API
1 parent 4b494c3 commit 1fcf840

18 files changed

+1499
-1014
lines changed

Example/Example/ContentView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ struct ContentView {
4343
wwdcKeynote: WWDCKeynote = .wwdc2024
4444
) {
4545
self.youTubePlayer = .init(
46-
urlString: wwdcKeynote.youTubeURL
46+
source: .init(urlString: wwdcKeynote.youTubeURL),
47+
isLoggingEnabled: true
4748
)
4849
self._wwdcKeynote = .init(initialValue: wwdcKeynote)
4950
}

Sources/Models/YouTubePlayer+APIError.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ public extension YouTubePlayer {
99

1010
// MARK: Properties
1111

12-
/// The JavaScript that has been executed and caused the error.
13-
public let javaScript: String
12+
/// The JavaScript code that has been executed and caused the error.
13+
public let javaScriptCode: YouTubePlayer.JavaScript.Code?
1414

1515
/// The optional JavaScript response.
1616
public let javaScriptResponse: String?
@@ -25,17 +25,17 @@ public extension YouTubePlayer {
2525

2626
/// Creates a new instance of ``YouTubePlayer.APIError``
2727
/// - Parameters:
28-
/// - javaScript: The JavaScript that has been executed and caused the error.
28+
/// - javaScriptCode: The JavaScript code that has been executed and caused the error. Default value `nil`
2929
/// - javaScriptResponse: The optional JavaScript response. Default value `nil`
3030
/// - underlyingError: The optional underlying error. Default value `nil`
3131
/// - reason: The optional error reason message. Default value `nil`
3232
public init(
33-
javaScript: String,
33+
javaScriptCode: YouTubePlayer.JavaScript.Code? = nil,
3434
javaScriptResponse: String? = nil,
3535
underlyingError: Swift.Error? = nil,
3636
reason: String? = nil
3737
) {
38-
self.javaScript = javaScript
38+
self.javaScriptCode = javaScriptCode
3939
self.javaScriptResponse = javaScriptResponse
4040
self.underlyingError = underlyingError
4141
self.reason = reason

Sources/Models/YouTubePlayer+Configuration.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ public extension YouTubePlayer {
3030
/// A custom user agent of the underlying web view.
3131
public var customUserAgent: String?
3232

33+
/// The HTML builder.
34+
public let htmlBuilder: HTMLBuilder
35+
3336
/// The action to perform when a url gets opened.
3437
public var openURLAction: OpenURLAction
3538

@@ -43,6 +46,7 @@ public extension YouTubePlayer {
4346
/// - useNonPersistentWebsiteDataStore: Boolean value indicating whether a non-persistent website data store should be used. Default value `true`
4447
/// - automaticallyAdjustsContentInsets: A Boolean value indicating if safe area insets should be added automatically to content insets. Default value `true`
4548
/// - customUserAgent: A custom user agent of the underlying web view. Default value `nil`
49+
/// - htmlBuilder: The HTML builder. Default value `.init()`
4650
/// - openURLAction: The action to perform when a url gets opened.. Default value `.default`
4751
public init(
4852
fullscreenMode: FullscreenMode = .preferred,
@@ -51,6 +55,7 @@ public extension YouTubePlayer {
5155
useNonPersistentWebsiteDataStore: Bool = true,
5256
automaticallyAdjustsContentInsets: Bool = true,
5357
customUserAgent: String? = nil,
58+
htmlBuilder: HTMLBuilder = .init(),
5459
openURLAction: OpenURLAction = .default
5560
) {
5661
self.fullscreenMode = fullscreenMode
@@ -59,6 +64,7 @@ public extension YouTubePlayer {
5964
self.useNonPersistentWebsiteDataStore = useNonPersistentWebsiteDataStore
6065
self.automaticallyAdjustsContentInsets = automaticallyAdjustsContentInsets
6166
self.customUserAgent = customUserAgent
67+
self.htmlBuilder = htmlBuilder
6268
self.openURLAction = openURLAction
6369
}
6470

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import Foundation
2+
3+
// MARK: - YouTubePlayer+HTMLBuilder
4+
5+
public extension YouTubePlayer {
6+
7+
/// A YouTube player HTML builder object.
8+
struct HTMLBuilder: Sendable {
9+
10+
// MARK: Typealias
11+
12+
/// The JSON encoded YouTube player options.
13+
public typealias JSONEncodedYouTubePlayerOptions = String
14+
15+
/// A function type that provides the HTML content string based on builder configuration and the JSON encoded YouTube player options.
16+
public typealias HTMLProvider = @Sendable (Self, JSONEncodedYouTubePlayerOptions) throws -> String
17+
18+
// MARK: Properties
19+
20+
/// The YouTube player JavaScrpt variable name.
21+
public var youTubePlayerJavaScriptVariableName: String
22+
23+
/// The YouTube player event callback url scheme.
24+
public var youTubePlayerEventCallbackURLScheme: String
25+
26+
/// The YouTube player event callback data parameter name.
27+
public var youTubePlayerEventCallbackDataParameterName: String
28+
29+
/// The YouTube player iFrame API source URL.
30+
public var youTubePlayerIframeAPISourceURL: URL
31+
32+
/// A closure providing the template
33+
public var htmlProvider: HTMLProvider
34+
35+
// MARK: Initializer
36+
37+
/// Creates a new instance of ``YouTubePlayer.HTMLBuilder``
38+
/// - Parameters:
39+
/// - youTubePlayerJavaScriptVariableName: The YouTube player JavaScrpt variable name. Default value `youtubePlayer`
40+
/// - youTubePlayerEventCallbackURLScheme: The YouTube player event callback url scheme. Default value `youtubeplayer`
41+
/// - youTubePlayerEventCallbackDataParameterName: The YouTube player event callback data parameter name. Default value `data`
42+
/// - youTubePlayerIframeAPISourceURL: The YouTube player iFrame API source URL. Default value `https://www.youtube.com/iframe_api`
43+
public init(
44+
youTubePlayerJavaScriptVariableName: String = "youtubePlayer",
45+
youTubePlayerEventCallbackURLScheme: String = "youtubeplayer",
46+
youTubePlayerEventCallbackDataParameterName: String = "data",
47+
youTubePlayerIframeAPISourceURL: URL = .init(string: "https://www.youtube.com/iframe_api")!,
48+
htmlProvider: @escaping HTMLProvider = Self.defaultHTMLProvider
49+
) {
50+
self.youTubePlayerJavaScriptVariableName = youTubePlayerJavaScriptVariableName
51+
self.youTubePlayerEventCallbackURLScheme = youTubePlayerEventCallbackURLScheme
52+
self.youTubePlayerEventCallbackDataParameterName = youTubePlayerEventCallbackDataParameterName
53+
self.youTubePlayerIframeAPISourceURL = youTubePlayerIframeAPISourceURL
54+
self.htmlProvider = htmlProvider
55+
}
56+
57+
}
58+
59+
}
60+
61+
// MARK: - Call as Function
62+
63+
public extension YouTubePlayer.HTMLBuilder {
64+
65+
/// Builds the HTML.
66+
/// - Parameter jsonEncodedYouTubePlayerOptions: The JSON encoded YouTube player options.
67+
func callAsFunction(
68+
jsonEncodedYouTubePlayerOptions: JSONEncodedYouTubePlayerOptions
69+
) throws -> String {
70+
return try self.htmlProvider(
71+
self,
72+
jsonEncodedYouTubePlayerOptions
73+
)
74+
}
75+
76+
}
77+
78+
// MARK: - Equatable
79+
80+
extension YouTubePlayer.HTMLBuilder: Equatable {
81+
82+
/// Returns a Boolean value indicating whether two values are equal.
83+
/// - Parameters:
84+
/// - lhs: A value to compare.
85+
/// - rhs: Another value to compare.
86+
public static func == (
87+
lhs: Self,
88+
rhs: Self
89+
) -> Bool {
90+
lhs.youTubePlayerJavaScriptVariableName == rhs.youTubePlayerJavaScriptVariableName
91+
&& lhs.youTubePlayerEventCallbackURLScheme == rhs.youTubePlayerEventCallbackURLScheme
92+
&& lhs.youTubePlayerEventCallbackDataParameterName == rhs.youTubePlayerEventCallbackDataParameterName
93+
&& lhs.youTubePlayerIframeAPISourceURL == rhs.youTubePlayerIframeAPISourceURL
94+
}
95+
96+
}
97+
98+
// MARK: - Hashable
99+
100+
extension YouTubePlayer.HTMLBuilder: Hashable {
101+
102+
/// Hashes the essential components of this value by feeding them into the given hasher.
103+
/// - Parameter hasher: The hasher to use when combining the components of this instance.
104+
public func hash(into hasher: inout Hasher) {
105+
hasher.combine(self.youTubePlayerJavaScriptVariableName)
106+
hasher.combine(self.youTubePlayerEventCallbackURLScheme)
107+
hasher.combine(self.youTubePlayerEventCallbackDataParameterName)
108+
hasher.combine(self.youTubePlayerIframeAPISourceURL)
109+
}
110+
111+
}
112+
113+
// MARK: - Default HTML Provider
114+
115+
public extension YouTubePlayer.HTMLBuilder {
116+
117+
/// The default HTML provider.
118+
static let defaultHTMLProvider: HTMLProvider = { htmlBuilder, jsonEncodedYouTubePlayerOptions in
119+
"""
120+
<!DOCTYPE html>
121+
<html>
122+
<head>
123+
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
124+
<style>
125+
body {
126+
margin: 0;
127+
width: 100%;
128+
height: 100%;
129+
}
130+
html {
131+
width: 100%;
132+
height: 100%;
133+
}
134+
.player-container iframe,
135+
.player-container object,
136+
.player-container embed {
137+
position: absolute;
138+
top: 0;
139+
left: 0;
140+
width: 100% !important;
141+
height: 100% !important;
142+
}
143+
::-webkit-scrollbar {
144+
display: none !important;
145+
}
146+
</style>
147+
</head>
148+
<body>
149+
<div class="player-container">
150+
<div id="\(htmlBuilder.youTubePlayerJavaScriptVariableName)"></div>
151+
</div>
152+
153+
<script src="\(htmlBuilder.youTubePlayerIframeAPISourceURL)"
154+
onerror="window.location.href='\(htmlBuilder.youTubePlayerEventCallbackURLScheme)://\(YouTubePlayer.JavaScriptEvent.Name.onIframeApiFailedToLoad.rawValue)'">
155+
</script>
156+
157+
<script>
158+
var \(htmlBuilder.youTubePlayerJavaScriptVariableName);
159+
160+
function onYouTubeIframeAPIReady() {
161+
\(htmlBuilder.youTubePlayerJavaScriptVariableName) = new YT.Player(
162+
'\(htmlBuilder.youTubePlayerJavaScriptVariableName)',
163+
\(jsonEncodedYouTubePlayerOptions)
164+
);
165+
\(htmlBuilder.youTubePlayerJavaScriptVariableName).setSize(
166+
window.innerWidth,
167+
window.innerHeight
168+
);
169+
sendYouTubePlayerEvent('\(YouTubePlayer.JavaScriptEvent.Name.onIframeApiReady.rawValue)');
170+
}
171+
172+
function sendYouTubePlayerEvent(eventName, event) {
173+
const url = new URL(`\(htmlBuilder.youTubePlayerEventCallbackURLScheme)://${eventName}`);
174+
if (event && event.data !== null) {
175+
url.searchParams.set('\(htmlBuilder.youTubePlayerEventCallbackDataParameterName)', event.data);
176+
}
177+
window.location.href = url.toString();
178+
}
179+
180+
\(
181+
YouTubePlayer
182+
.JavaScriptEvent
183+
.Name
184+
.allCases
185+
.filter { $0 != .onIframeApiReady && $0 != .onIframeApiFailedToLoad }
186+
.map { javaScriptEventName in
187+
"""
188+
function \(javaScriptEventName)(event) {
189+
sendYouTubePlayerEvent('\(javaScriptEventName)', event);
190+
}
191+
"""
192+
}
193+
.joined(separator: "\n\n")
194+
)
195+
</script>
196+
</body>
197+
</html>
198+
"""
199+
}
200+
201+
}

0 commit comments

Comments
 (0)