SwiftUI WebKit Integration
Overview
SwiftUI provides native integration with WebKit through the WebView struct, allowing developers to embed web content directly within SwiftUI applications. This integration enables displaying HTML, CSS, and JavaScript content alongside native views, with support for full browsing experiences including navigation, JavaScript execution, and customization options.
Key components:
WebView: A SwiftUI view that displays web contentWebPage: An observable class that controls and manages web content behavior- JavaScript interaction capabilities
- Navigation management
- Customization options
WebView Basics
Creating a Basic WebView
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
WebView(url: URL(string: "https://www.apple.com"))
.frame(height: 400)
}
}Toggling Between Different URLs
struct ContentView: View {
@State private var toggle = false
private var url: URL? {
toggle ? URL(string: "https://www.webkit.org") : URL(string: "https://www.swift.org")
}
var body: some View {
WebView(url: url)
.toolbar {
Button(toggle ? "Show Swift" : "Show WebKit",
systemImage: toggle ? "swift" : "network") {
toggle.toggle()
}
}
}
}Using WebView with WebPage
struct ContentView: View {
@State private var page = WebPage()
var body: some View {
NavigationStack {
WebView(page)
.navigationTitle(page.title)
}
.onAppear {
if let url = URL(string: "https://www.apple.com") {
let _ = page.load(URLRequest(url: url))
}
}
}
}Enabling Text Search in a WebView
Use the findNavigator(isPresented:) modifier to enable text search within the WebView content.
import SwiftUI
import WebKit
struct ContentView: View {
@State private var searchVisible = true
var body: some View {
WebView(url: URL(string: "https://www.apple.com"))
.frame(height: 400)
.findNavigator(isPresented: $searchVisible)
}
}WebPage Configuration
Creating a WebPage with Configuration
var configuration = WebPage.Configuration()
configuration.loadsSubresources = true
configuration.defaultNavigationPreferences.allowsContentJavaScript = true
configuration.websiteDataStore = .default()
let page = WebPage(configuration: configuration)Using Non-Persistent Data Store
var configuration = WebPage.Configuration()
configuration.websiteDataStore = .nonPersistent()
let page = WebPage(configuration: configuration)Setting Custom User Agent
let page = WebPage()
page.customUserAgent = "MyApp/1.0 CustomUserAgent"Configuring JavaScript Permissions
var configuration = WebPage.Configuration()
configuration.defaultNavigationPreferences.allowsContentJavaScript = true
let page = WebPage(configuration: configuration)Navigation Management
Loading Content
// Load from URL
let url = URL(string: "https://www.example.com")!
let navigationID = page.load(URLRequest(url: url))
// Load HTML string
let html = "<html><body><h1>Hello World</h1></body></html>"
let navigationID = page.load(html: html, baseURL: URL(string: "https://www.example.com")!)
// Load from Data
let data = Data() // Your data
let navigationID = page.load(data, mimeType: "text/html", characterEncoding: .utf8, baseURL: URL(string: "https://www.example.com")!)Navigation Controls
// Reload page
let navigationID = page.reload(fromOrigin: false)
// Stop loading
page.stopLoading()
// Access back-forward list
let canGoBack = !page.backForwardList.backList.isEmpty
let canGoForward = !page.backForwardList.forwardList.isEmpty
// Navigate back or forward
if let backItem = page.backForwardList.backItem {
let navigationID = page.load(backItem)
}Observing Navigation Events
struct ContentView: View {
@State private var page = WebPage()
@State private var isLoading = false
var body: some View {
VStack {
WebView(page)
if isLoading {
ProgressView()
.progressViewStyle(.circular)
}
}
.onChange(of: page.currentNavigationEvent) { _, newEvent in
if let event = newEvent {
switch event.state {
case .started:
isLoading = true
case .finished, .failed:
isLoading = false
default:
break
}
}
}
}
}Customizing Navigation Behavior
struct MyNavigationDecider: WebPage.NavigationDeciding {
func decidePolicyFor(navigationAction: WebPage.NavigationAction) async -> WebPage.NavigationPreferences? {
// Block navigation to specific domains
if let url = navigationAction.request.url, url.host == "blocked-site.com" {
return nil // Return nil to cancel navigation
}
// Allow navigation with custom preferences
var preferences = WebPage.NavigationPreferences()
preferences.allowsContentJavaScript = true
return preferences
}
func decidePolicyFor(navigationResponse: WebPage.NavigationResponse) async -> Bool {
// Check response status code
if let httpResponse = navigationResponse.response as? HTTPURLResponse {
return httpResponse.statusCode == 200
}
return true
}
}
// Use the custom navigation decider
let page = WebPage(configuration: WebPage.Configuration(),
navigationDecider: MyNavigationDecider())JavaScript Interaction
Executing JavaScript
// Basic JavaScript execution
let script = "document.title"
do {
let title = try await page.callJavaScript(script)
print("Page title: \(title)")
} catch {
print("Error executing JavaScript: \(error)")
}Passing Arguments to JavaScript
let script = """
function findElement(selector) {
return document.querySelector(selector)?.textContent;
}
return findElement(selector);
"""
let arguments = ["selector": ".main-content h1"]
do {
let result = try await page.callJavaScript(script, arguments: arguments)
if let headingText = result as? String {
print("Heading text: \(headingText)")
}
} catch {
print("Error executing JavaScript: \(error)")
}Executing JavaScript in Specific Frame
// Get main frame info
if let mainFrame = page.currentNavigationEvent?.frameInfo {
let script = "document.body.innerHTML"
do {
let html = try await page.callJavaScript(script, in: mainFrame)
print("HTML content: \(html)")
} catch {
print("Error executing JavaScript: \(error)")
}
}Using Content Worlds for Isolated JavaScript Execution
import WebKit
let script = "document.title"
let contentWorld = WKContentWorld.page // or .defaultClient or a custom world
do {
let title = try await page.callJavaScript(script, contentWorld: contentWorld)
print("Page title: \(title)")
} catch {
print("Error executing JavaScript: \(error)")
}Extracting Metadata Example
func fetchMetadata(for url: URL) async throws -> (title: String, description: String) {
let page = WebPage()
let request = URLRequest(url: url)
let navigationID = page.load(request)
// Wait for the page to load by tracking navigation events
while page.isLoading {
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
}
// Extract title
guard let title = page.title else {
throw URLError(.cannotParseResponse)
}
// Extract description using JavaScript
let fetchDescription = """
const metaDescription = document.querySelector('meta[name="description"]');
return metaDescription ? metaDescription.getAttribute('content') : '';
"""
guard let description = try await page.callJavaScript(fetchDescription) as? String else {
throw URLError(.cannotParseResponse)
}
return (title, description)
}Customization Options
Disabling Navigation Gestures
WebView(url: url)
.webViewBackForwardNavigationGestures(.disabled)Configuring Magnification Gestures
WebView(url: url)
.webViewMagnificationGestures(.enabled)Customizing Link Previews
WebView(url: url)
.webViewLinkPreviews(.disabled)Configuring Text Selection
WebView(url: url)
.webViewTextSelection(.enabled)Setting Content Background
WebView(url: url)
.webViewContentBackground(.color(.systemBackground))Customizing Context Menu
WebView(url: url)
.webViewContextMenu { defaultActions in
// Filter or modify default actions
let filteredActions = defaultActions.filter { action in
// Only allow copy and share actions
return action.identifier == .copy || action.identifier == .share
}
// Add custom actions
let customAction = UIAction(title: "Custom Action") { _ in
print("Custom action triggered")
}
return filteredActions + [customAction]
}Handling Fullscreen Content
WebView(url: url)
.webViewElementFullscreenBehavior(.enabled)Advanced Features
Capturing Web Content as Image
func captureWebViewSnapshot(from page: WebPage) async -> Image? {
do {
let config = WKSnapshotConfiguration()
config.rect = CGRect(x: 0, y: 0, width: 1024, height: 768)
return try await page.snapshot(config)
} catch {
print("Error capturing snapshot: \(error)")
return nil
}
}Generating PDF from Web Content
func generatePDF(from page: WebPage) async -> Data? {
do {
let config = WKPDFConfiguration()
return try await page.pdf(configuration: config)
} catch {
print("Error generating PDF: \(error)")
return nil
}
}Creating Web Archive
func createWebArchive(from page: WebPage) async -> Data? {
do {
return try await page.webArchiveData()
} catch {
print("Error creating web archive: \(error)")
return nil
}
}Custom URL Scheme Handling
struct MyURLSchemeHandler: URLSchemeHandler {
func start(task: URLSchemeTask) {
guard let url = task.request.url, url.scheme == "myapp" else {
task.didFailWithError(URLError(.badURL))
return
}
// Handle custom URL scheme
let response = URLResponse(url: url,
mimeType: "text/html",
expectedContentLength: -1,
textEncodingName: "utf-8")
let html = "<html><body><h1>Custom Scheme Content</h1></body></html>"
let data = Data(html.utf8)
task.didReceive(response)
task.didReceive(data)
task.didFinish()
}
func stop(task: URLSchemeTask) {
// Handle task cancellation
}
}
// Register the custom URL scheme handler
var configuration = WebPage.Configuration()
configuration.setURLSchemeHandler(MyURLSchemeHandler(), forURLScheme: "myapp")
let page = WebPage(configuration: configuration)
// Load content with custom scheme
let customURL = URL(string: "myapp://content")!
let _ = page.load(URLRequest(url: customURL))