@@ -3,6 +3,8 @@ import UIKit
33import WebKit
44
55struct YoutubePlayerView : UIViewRepresentable {
6+ @Environment ( \. openURL) private var openURL
7+
68 let videoURL : URL ?
79 let startSeconds : Double
810 let endSeconds : Double
@@ -32,17 +34,47 @@ struct YoutubePlayerView: UIViewRepresentable {
3234 webView. backgroundColor = . clear
3335 webView. isUserInteractionEnabled = true
3436 webView. allowsLinkPreview = false
37+ webView. navigationDelegate = context. coordinator
38+ webView. uiDelegate = context. coordinator
39+
40+ let redirectOverlayButton = UIButton ( type: . custom)
41+ redirectOverlayButton. translatesAutoresizingMaskIntoConstraints = false
42+ redirectOverlayButton. backgroundColor = . clear
43+ redirectOverlayButton. accessibilityLabel = " 유튜브에서 열기 "
44+ redirectOverlayButton. addTarget (
45+ context. coordinator,
46+ action: #selector( Coordinator . handleVideoTap) ,
47+ for: . touchUpInside
48+ )
49+ webView. addSubview ( redirectOverlayButton)
50+ NSLayoutConstraint . activate ( [
51+ redirectOverlayButton. topAnchor. constraint ( equalTo: webView. topAnchor) ,
52+ redirectOverlayButton. leadingAnchor. constraint ( equalTo: webView. leadingAnchor) ,
53+ redirectOverlayButton. trailingAnchor. constraint ( equalTo: webView. trailingAnchor) ,
54+ redirectOverlayButton. bottomAnchor. constraint ( equalTo: webView. bottomAnchor) ,
55+ ] )
56+
57+ context. coordinator. openExternalURL = { targetURL in
58+ openURL ( targetURL)
59+ }
3560 return webView
3661 }
3762
3863 func updateUIView( _ webView: WKWebView , context: Context ) {
64+ context. coordinator. openExternalURL = { targetURL in
65+ openURL ( targetURL)
66+ }
67+
3968 guard
4069 let videoURL,
4170 let videoID = extractVideoID ( from: videoURL)
4271 else {
72+ context. coordinator. redirectURL = nil
4373 return
4474 }
4575
76+ context. coordinator. redirectURL = makeWatchURL ( videoID: videoID) ?? videoURL
77+
4678 let targetStart = normalizedSeconds ( startSeconds)
4779 let targetEnd = max ( normalizedSeconds ( endSeconds) , targetStart + 0.1 )
4880 if context. coordinator. loadedVideoID != videoID {
@@ -131,11 +163,53 @@ struct YoutubePlayerView: UIViewRepresentable {
131163 Coordinator ( )
132164 }
133165
134- final class Coordinator {
166+ final class Coordinator : NSObject , WKNavigationDelegate , WKUIDelegate {
135167 var loadedVideoID : String ?
136168 var lastSyncedStart : Double ?
137169 var lastSyncedEnd : Double ?
138170 var lastSyncedIsPlaying : Bool ?
171+ var redirectURL : URL ?
172+ var openExternalURL : ( ( URL ) -> Void ) ?
173+
174+ @objc
175+ func handleVideoTap( ) {
176+ openRedirectURL ( )
177+ }
178+
179+ func webView(
180+ _ webView: WKWebView ,
181+ decidePolicyFor navigationAction: WKNavigationAction ,
182+ decisionHandler: @escaping ( WKNavigationActionPolicy ) -> Void
183+ ) {
184+ let isUserNavigation =
185+ navigationAction. navigationType == . linkActivated
186+ || navigationAction. navigationType == . formSubmitted
187+ || navigationAction. navigationType == . formResubmitted
188+ || navigationAction. targetFrame == nil
189+
190+ guard isUserNavigation else {
191+ decisionHandler ( . allow)
192+ return
193+ }
194+
195+ openRedirectURL ( )
196+ decisionHandler ( . cancel)
197+ }
198+
199+ func webView(
200+ _ webView: WKWebView ,
201+ createWebViewWith configuration: WKWebViewConfiguration ,
202+ for navigationAction: WKNavigationAction ,
203+ windowFeatures: WKWindowFeatures
204+ ) -> WKWebView ? {
205+ openRedirectURL ( )
206+ return nil
207+ }
208+
209+ private func openRedirectURL( ) {
210+ guard let redirectURL else { return }
211+ openExternalURL ? ( redirectURL)
212+ }
139213 }
140214
141215 private var appRefererURL : URL ? {
@@ -384,6 +458,31 @@ struct YoutubePlayerView: UIViewRepresentable {
384458 }
385459 }
386460
461+ if let shortsIndex = pathComponents. firstIndex ( of: " shorts " ) ,
462+ pathComponents. indices. contains ( shortsIndex + 1 ) {
463+ let candidate = pathComponents [ shortsIndex + 1 ]
464+ if !candidate. isEmpty {
465+ return candidate
466+ }
467+ }
468+
469+ if let liveIndex = pathComponents. firstIndex ( of: " live " ) ,
470+ pathComponents. indices. contains ( liveIndex + 1 ) {
471+ let candidate = pathComponents [ liveIndex + 1 ]
472+ if !candidate. isEmpty {
473+ return candidate
474+ }
475+ }
476+
477+ if
478+ let host = URLComponents ( url: url, resolvingAgainstBaseURL: false ) ? . host? . lowercased ( ) ,
479+ host. contains ( " youtu.be " ) ,
480+ let firstPath = pathComponents. first,
481+ !firstPath. isEmpty
482+ {
483+ return firstPath
484+ }
485+
387486 if let components = URLComponents ( url: url, resolvingAgainstBaseURL: false ) ,
388487 let v = components. queryItems? . first ( where: { $0. name == " v " } ) ? . value,
389488 !v. isEmpty {
@@ -427,4 +526,11 @@ struct YoutubePlayerView: UIViewRepresentable {
427526 private func jsNumber( _ value: Double ) -> String {
428527 String ( format: " %.3f " , value)
429528 }
529+
530+ private func makeWatchURL( videoID: String ) -> URL ? {
531+ guard !videoID. isEmpty else { return nil }
532+ var components = URLComponents ( string: " https://www.youtube.com/watch " )
533+ components? . queryItems = [ URLQueryItem ( name: " v " , value: videoID) ]
534+ return components? . url
535+ }
430536}
0 commit comments