Issue #1017
When your app loads web content with embedded iframes, deciding which navigations to allow becomes surprisingly tricky. A click in an ad banner behaves differently than a click on the main page content, and WKWebView’s delegate methods give you the tools to tell them apart.
In this article, we’ll explore how to use sourceFrame and targetFrame properties to detect where navigation events originate and where they intend to go. You’ll learn to distinguish main frame actions from iframe activity—essential knowledge for building secure, well-behaved web views.
Understanding frames in web content
Web pages aren’t always single documents. The <iframe> element lets developers embed external content—ads, videos, widgets, or even entire websites—inside a parent page. Each iframe creates its own browsing context with separate navigation history.
Here’s a typical webpage structure with iframes:
<!DOCTYPE html>
<html>
<head>
<title>Main Page</title>
</head>
<body>
<h1>Welcome to My App</h1>
<p>This content lives in the main frame.</p>
<!-- Embedded content in iframes -->
<iframe id="ad-banner" src="https://ads.example.com/banner"></iframe>
<iframe id="video-player" src="https://player.example.com/embed/12345"></iframe>
<a href="/about">About Us</a>
</body>
</html>
When a user taps a link in this page, WKWebView needs to know: did they tap the “About Us” link in the main content, or did they tap something inside one of those iframes? The answer determines whether your app should navigate, open Safari, or block the request entirely.
The WKFrameInfo object
Before diving into navigation handling, let’s understand what WKWebView tells us about frames. The WKFrameInfo class provides metadata about any frame in your web content:
// WKFrameInfo key properties
frameInfo.isMainFrame // Bool - true if this is the top-level document
frameInfo.request // URLRequest - the request that loaded this frame
frameInfo.securityOrigin // WKSecurityOrigin - origin info for security checks
frameInfo.webView // WKWebView? - the web view containing this frame
The isMainFrame property is your primary tool for frame detection. When it returns true, you’re dealing with the top-level document. When false, the frame is an iframe embedded somewhere in the page hierarchy.
Navigation delegate methods that expose frame information
WKWebView provides frame information through two key delegate methods. Each serves a different purpose in the navigation lifecycle.
decidePolicyFor navigationAction
This method fires before any navigation begins, giving you the chance to allow or cancel it:
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
let sourceFrame = navigationAction.sourceFrame
let targetFrame = navigationAction.targetFrame
print("Source is main frame: \(sourceFrame.isMainFrame)")
print("Target is main frame: \(targetFrame?.isMainFrame ?? false)")
decisionHandler(.allow)
}
That does a few important things:
sourceFrametells you which frame initiated the navigation—where the user clicked or where JavaScript calledwindow.locationtargetFrametells you where the content should load, which might benilfor new window requests- The decision handler lets you allow, cancel, or download the request
createWebViewWith configuration
When web content wants to open a new window (via target="_blank" or JavaScript’s window.open), this WKUIDelegate method fires:
func webView(
_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures
) -> WKWebView? {
// Check if this should open in the current view
if let frame = navigationAction.targetFrame, frame.isMainFrame {
return nil // Let normal navigation proceed
}
// Handle the new window request in your current web view
if let url = navigationAction.request.url {
webView.load(URLRequest(url: url))
}
return nil // Don't create a new WKWebView
}
Important: You must set
webView.uiDelegate = selffor this method to be called. Without it,target="_blank"links will silently fail.
sourceFrame vs targetFrame: when to use each
Understanding the distinction between these properties is crucial for correct navigation handling.
sourceFrame: where did the action originate?
The sourceFrame property answers “which frame triggered this navigation?” Use it when you need to know the context of user interaction:
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
let source = navigationAction.sourceFrame
if source.isMainFrame {
// User clicked something in the main page content
print("Navigation initiated from main document")
} else {
// Click happened inside an iframe
print("Navigation initiated from embedded iframe")
print("Iframe origin: \(source.securityOrigin.host)")
}
decisionHandler(.allow)
}
Common scenarios where sourceFrame matters:
- Ad blocking: Reject navigations that originate from known ad iframe domains
- Analytics: Track whether users engage with main content or embedded widgets
- Security: Apply stricter policies to navigation requests from third-party iframes
targetFrame: where should content load?
The targetFrame property answers “where will this content appear?” It can be nil when content wants to open in a new window:
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
if let target = navigationAction.targetFrame {
if target.isMainFrame {
// Content will replace the entire page
print("Full page navigation")
} else {
// Content will load inside an iframe
print("Iframe content loading")
}
} else {
// No target frame means new window/tab request
print("New window requested")
}
decisionHandler(.allow)
}
When targetFrame is nil, the web content is trying to open a new browsing context. You’ll typically handle this in createWebViewWith instead.
Practical use cases
Let’s look at real-world scenarios where frame checking becomes essential.
Blocking iframe navigation attempts
Prevent iframes from hijacking your web view’s main content:
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
let source = navigationAction.sourceFrame
let target = navigationAction.targetFrame
// Block if an iframe tries to navigate the main frame
if !source.isMainFrame && (target?.isMainFrame ?? false) {
print("Blocked: iframe attempted to navigate main frame")
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
This pattern protects against clickjacking attacks where malicious iframes try to redirect users away from your intended content.
Handling link clicks inside iframes
Sometimes you want iframe links to open in Safari rather than your embedded web view:
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
guard navigationAction.navigationType == .linkActivated else {
decisionHandler(.allow)
return
}
let source = navigationAction.sourceFrame
// Links clicked in iframes open externally
if !source.isMainFrame, let url = navigationAction.request.url {
UIApplication.shared.open(url)
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
Intercepting iframe loads for content filtering
Monitor what iframes are loading to enforce content policies:
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
let target = navigationAction.targetFrame
// Only check iframe loads, not main frame
if let target = target, !target.isMainFrame {
guard let url = navigationAction.request.url,
let host = url.host else {
decisionHandler(.allow)
return
}
let blockedDomains = ["ads.example.com", "tracker.example.com"]
if blockedDomains.contains(where: { host.contains($0) }) {
print("Blocked iframe load: \(host)")
decisionHandler(.cancel)
return
}
}
decisionHandler(.allow)
}
Complete delegate implementation
Here’s a production-ready implementation combining these patterns:
final class WebViewController: UIViewController {
private let webView: WKWebView
init() {
let configuration = WKWebViewConfiguration()
self.webView = WKWebView(frame: .zero, configuration: configuration)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(webView)
webView.frame = view.bounds
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
webView.navigationDelegate = self
webView.uiDelegate = self // Required for createWebViewWith
}
}
extension WebViewController: WKNavigationDelegate {
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
let source = navigationAction.sourceFrame
let target = navigationAction.targetFrame
// Log frame information for debugging
logFrameInfo(source: source, target: target)
// Security: block iframe-to-main-frame navigation hijacking
if !source.isMainFrame && (target?.isMainFrame ?? false) {
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
private func logFrameInfo(source: WKFrameInfo, target: WKFrameInfo?) {
print("""
Navigation:
Source: \(source.isMainFrame ? "main" : "iframe") (\(source.securityOrigin.host))
Target: \(target?.isMainFrame ?? false ? "main" : "iframe/new window")
""")
}
}
extension WebViewController: WKUIDelegate {
func webView(
_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures
) -> WKWebView? {
// Handle target="_blank" and window.open
if let frame = navigationAction.targetFrame, frame.isMainFrame {
return nil
}
// Load in current web view instead of opening new window
if let url = navigationAction.request.url {
webView.load(URLRequest(url: url))
}
return nil
}
}
Web code examples: testing iframe scenarios
To test your frame handling logic, create an HTML page with various iframe configurations:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Frame Testing Page</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; }
iframe { border: 2px solid #007AFF; margin: 10px 0; }
.section { margin: 20px 0; padding: 15px; background: #f5f5f7; border-radius: 8px; }
h2 { color: #1d1d1f; }
</style>
</head>
<body>
<h1>WKWebView Frame Testing</h1>
<div class="section">
<h2>Main Frame Links</h2>
<p>These links are in the main frame:</p>
<a href="https://apple.com">Apple (same window)</a><br>
<a href="https://developer.apple.com" target="_blank">Developer (new window)</a>
</div>
<div class="section">
<h2>Iframe with Links</h2>
<iframe id="link-frame" srcdoc="
<html>
<body style='font-family: sans-serif; padding: 10px;'>
<p>Links inside iframe:</p>
<a href='https://webkit.org'>WebKit (iframe navigation)</a><br>
<a href='https://swift.org' target='_top'>Swift (targets main frame)</a><br>
<a href='https://github.com' target='_blank'>GitHub (new window from iframe)</a>
</body>
</html>
" width="100%" height="150"></iframe>
</div>
<div class="section">
<h2>JavaScript Navigation Tests</h2>
<button onclick="testMainFrameJS()">Navigate Main Frame (JS)</button>
<button onclick="testIframeJS()">Navigate Iframe (JS)</button>
<iframe id="js-frame" src="about:blank" width="100%" height="100"></iframe>
</div>
<script>
function testMainFrameJS() {
// This navigates the main frame via JavaScript
window.location.href = 'https://example.com';
}
function testIframeJS() {
// This navigates only the iframe
const iframe = document.getElementById('js-frame');
iframe.src = 'https://example.org';
}
// Test iframe trying to navigate parent (security concern)
window.addEventListener('message', function(event) {
console.log('Message from iframe:', event.data);
});
</script>
</body>
</html>
Testing different target attributes
The HTML target attribute affects how targetFrame and isMainFrame behave:
| Target Attribute | targetFrame | isMainFrame | Behavior |
|---|---|---|---|
| (none) | Current frame | Depends on context | Navigates the frame containing the link |
_self |
Current frame | Depends on context | Same as no target |
_parent |
Parent frame | true if parent is main |
Navigates the parent frame |
_top |
Main frame | true |
Always navigates the top-level document |
_blank |
nil |
N/A | Opens new window (triggers createWebViewWith) |
JavaScript that tests frame relationships
Add this script to your test page to explore frame hierarchy programmatically:
// Check if current context is the top window
function isMainFrame() {
return window === window.top;
}
// Get information about the current frame context
function getFrameInfo() {
return {
isMain: window === window.top,
isFramed: window !== window.top,
parentOrigin: window.parent !== window ? getParentOrigin() : null,
depth: getFrameDepth()
};
}
function getParentOrigin() {
try {
return window.parent.location.origin;
} catch (e) {
return 'cross-origin (blocked)';
}
}
function getFrameDepth() {
let depth = 0;
let current = window;
while (current !== current.top) {
depth++;
current = current.parent;
}
return depth;
}
// Log frame info on page load
console.log('Frame Info:', JSON.stringify(getFrameInfo(), null, 2));
Frame detection in WKWebView comes down to understanding two properties:
- sourceFrame.isMainFrame: Did the action originate in the main document or an embedded iframe?
- targetFrame?.isMainFrame: Will the resulting content load in the main document, an iframe, or a new window?
By combining these checks, you can build sophisticated navigation policies that protect users from malicious content while preserving legitimate iframe functionality. Whether you’re blocking ad-based redirects, enforcing content security policies, or simply handling target="_blank" links gracefully, frame information gives you the context you need to make informed decisions
Start the conversation