Skip to content

Commit 9b07d9f

Browse files
authored
Feat - 유튜브 리다이렉팅 이벤트 추가 (#56)
* feat(YoutubePlayer): 웹 리다이렉팅 추가 * feat(1.0.23): 버전 업데이트
1 parent 43acf1d commit 9b07d9f

2 files changed

Lines changed: 111 additions & 5 deletions

File tree

KillingPart.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@
434434
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
435435
CODE_SIGN_ENTITLEMENTS = KillingPart/KillingPart.entitlements;
436436
CODE_SIGN_STYLE = Automatic;
437-
CURRENT_PROJECT_VERSION = 22;
437+
CURRENT_PROJECT_VERSION = 23;
438438
DEAD_CODE_STRIPPING = YES;
439439
DEVELOPMENT_TEAM = GQ89YG5G9R;
440440
ENABLE_APP_SANDBOX = YES;
@@ -459,7 +459,7 @@
459459
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
460460
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
461461
MACOSX_DEPLOYMENT_TARGET = 14.0;
462-
MARKETING_VERSION = 1.0.22;
462+
MARKETING_VERSION = 1.0.23;
463463
PRODUCT_BUNDLE_IDENTIFIER = com.killingpoint.killingpart;
464464
PRODUCT_NAME = "$(TARGET_NAME)";
465465
REGISTER_APP_GROUPS = YES;
@@ -479,7 +479,7 @@
479479
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
480480
CODE_SIGN_ENTITLEMENTS = KillingPart/KillingPart.entitlements;
481481
CODE_SIGN_STYLE = Automatic;
482-
CURRENT_PROJECT_VERSION = 22;
482+
CURRENT_PROJECT_VERSION = 23;
483483
DEAD_CODE_STRIPPING = YES;
484484
DEVELOPMENT_TEAM = GQ89YG5G9R;
485485
ENABLE_APP_SANDBOX = YES;
@@ -504,7 +504,7 @@
504504
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
505505
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
506506
MACOSX_DEPLOYMENT_TARGET = 14.0;
507-
MARKETING_VERSION = 1.0.22;
507+
MARKETING_VERSION = 1.0.23;
508508
PRODUCT_BUNDLE_IDENTIFIER = com.killingpoint.killingpart;
509509
PRODUCT_NAME = "$(TARGET_NAME)";
510510
REGISTER_APP_GROUPS = YES;

KillingPart/Views/Screens/Main/Add/AddSearchDetail/components/YoutubePlayerView.swift

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import UIKit
33
import WebKit
44

55
struct 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

Comments
 (0)