How to Take a Screenshot of a Website in Swift
Render a page in WKWebView, capture it with WebKit’s asynchronous snapshot API, and handle image size, loading, errors, and PDF output.

To capture a website displayed in a Swift app, load it in a WKWebView and call WebKit’s asynchronous takeSnapshot API. It returns a platform-native image: UIImage on iOS-family platforms, or NSImage on macOS. Configure the snapshot rectangle and output width with WKSnapshotConfiguration. If you need a PDF document instead of an image, use WKWebView.pdf(configuration:). Apple’s WKWebView documentation describes how the view loads and renders web content; see the API references for takeSnapshot and WKSnapshotConfiguration.
1. Create a WKWebView and load the page
WKWebView is the WebKit view that renders the page. The snapshot operation belongs to that view, so you capture its rendered content rather than asking iOS or macOS to capture the whole device screen. The smallest implementation needs a web view, a URL request, and a call to takeSnapshot after the page has finished loading.
Here is a complete UIKit example for an iOS app. Add the file to an iOS target, then make ScreenshotViewController the screen you present. The example uses a delegate callback to start the capture after the main navigation finishes.
import UIKit
import WebKit
@MainActor
final class ScreenshotViewController: UIViewController, WKNavigationDelegate {
private var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: .zero)
webView.navigationDelegate = self
webView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(webView)
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
guard let url = URL(string: "https://example.com") else { return }
webView.load(URLRequest(url: url))
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
captureVisibleRegion()
}
private func captureVisibleRegion() {
let configuration = WKSnapshotConfiguration()
configuration.rect = webView.bounds
configuration.afterScreenUpdates = true
webView.takeSnapshot(with: configuration) { [weak self] image, error in
guard let self else { return }
guard let image else {
print("Snapshot failed: \(error?.localizedDescription ?? \"No image returned\")")
return
}
// Use the UIImage here, or encode it for storage or sharing.
self.savePNG(image)
}
}
private func savePNG(_ image: UIImage) {
guard let data = image.pngData() else {
print("Could not encode image as PNG")
return
}
do {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("website-screenshot.png")
try data.write(to: url, options: .atomic)
print("Saved screenshot to \(url.path)")
} catch {
print("Could not save screenshot: \(error.localizedDescription)")
}
}
}
The code saves to the app’s temporary directory, which is suitable for a short-lived file. For a file that should remain in the user’s Documents area, use your app’s file-export or document-picker flow instead. For sharing, pass the resulting image or file URL to the share interface.
2. Choose the capture region and image size
WKSnapshotConfiguration controls the capture. Its rect is in the web view’s coordinate system; its snapshotWidth specifies the output width in points. The API documentation also describes afterScreenUpdates, which asks WebKit to incorporate pending screen updates. That setting does not mean the browser waits for every image, font, animation, or network request to finish.

| Setting | Use it for | Practical detail |
|---|---|---|
rect |
Capturing a region of the web view | Set it in the web view’s coordinate system. For the visible bounds, use webView.bounds. |
snapshotWidth |
Choosing the output width in points | Set it when you need a deliberate output width. Consider memory use when choosing a large value. |
afterScreenUpdates |
Including pending view updates | Set it to true when a recent layout or visual update should be reflected. |
For example, to capture a rectangle inset from the visible bounds:
let configuration = WKSnapshotConfiguration()
configuration.rect = CGRect(
x: 0,
y: 100,
width: webView.bounds.width,
height: 500
)
configuration.snapshotWidth = 800
configuration.afterScreenUpdates = true
webView.takeSnapshot(with: configuration) { image, error in
guard let image else {
print("Capture failed: \(error?.localizedDescription ?? \"unknown error\")")
return
}
// Handle the platform-native image.
}
Check that the rectangle is meaningful for the current web view size. A view that has not been laid out yet can have zero or stale bounds. If the view’s size changes during rotation or resizing, calculate the rectangle after layout. A larger configured width can produce a more detailed image, but it can also require more memory and take longer to encode or save.
3. Wait for the content you need
The navigation delegate’s didFinish callback is a useful point to begin a basic capture, but a page can continue changing after navigation completes. JavaScript may insert content, lazy-loaded images may appear only after scrolling, and animations can keep moving. afterScreenUpdates incorporates pending screen updates; it is not a page-readiness or network-idle guarantee.
If the page is yours, add a clear readiness signal in its JavaScript and wait for it before taking the snapshot. For a page you do not control, a short delay can help with known late rendering, but a fixed delay is only a heuristic: too short can miss content, while too long wastes time.
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState") { [weak self] result, error in
guard let self else { return }
guard error == nil else {
self.captureVisibleRegion()
return
}
// Navigation has completed. This is not proof that every asset
// or asynchronous page update has completed.
self.captureVisibleRegion()
}
}
For a page you control, you can evaluate a page-specific readiness expression instead, such as checking that a known element exists. Handle JavaScript evaluation errors and provide a timeout path so a page that never reaches the desired state does not leave the capture workflow stuck. Avoid treating document.readyState alone as proof that fonts, images, or application data are ready.
4. Use Swift concurrency where available
Apple also documents an async snapshot overload. It can make the capture flow easier to compose with other asynchronous work. The image type still depends on the platform, and the API can throw an error, so handle failure with do/catch.
func captureWithAsyncAPI() async {
let configuration = WKSnapshotConfiguration()
configuration.rect = webView.bounds
configuration.afterScreenUpdates = true
do {
let image = try await webView.takeSnapshot(configuration: configuration)
// On iOS-family platforms, use the returned UIImage.
savePNG(image)
} catch {
print("Snapshot failed: \(error.localizedDescription)")
}
}
Use the completion-handler version if it better fits your deployment target or callback-based code. Check the WebKit API available to the SDK and operating system versions you support before choosing an overload. Keep UI work on the main actor, and avoid force-unwrapping the returned image or a URL.
5. Capture a PDF instead of a raster image
A PDF is a separate output choice. WebKit’s pdf(configuration:) asynchronously generates PDF data using WKPDFConfiguration. Choose it when the result should be a document. Do not expect a PDF API call to return a UIImage or NSImage.
import WebKit
func makePDF(from webView: WKWebView) {
let configuration = WKPDFConfiguration()
webView.pdf(configuration: configuration) { result in
switch result {
case .success(let data):
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("website.pdf")
do {
try data.write(to: url, options: .atomic)
print("Saved PDF to \(url.path)")
} catch {
print("Could not save PDF: \(error.localizedDescription)")
}
case .failure(let error):
print("PDF generation failed: \(error.localizedDescription)")
}
}
}
For the available PDF configuration and behavior, see Apple’s WKWebView PDF API documentation. If your requirement is a shareable or printable document, evaluate the PDF output on the pages and platforms your app supports.
6. Platform and full-page considerations
On iOS, iPadOS, Mac Catalyst, and visionOS, the documented snapshot result is a UIImage. On macOS it is an NSImage. Code that is shared across these targets should not assume UIKit image types everywhere; use platform-specific handling or isolate the image conversion behind a small platform-specific function.
There is an important distinction between capturing the web view’s configured region and capturing an entire long web page. The documented configuration takes a rectangle in the web view’s coordinate system; it does not establish a universal guarantee that an arbitrary page extending beyond the view’s displayed area will be captured as one full-page image. If you need full-page output, verify behavior for the OS versions, layout, and content you ship. Consider whether a PDF is a better fit for a document-length result.
Also consider what the site can display in an embedded web view. Authentication, consent dialogs, bot checks, responsive layout, and cross-origin resources can affect the rendered page. A snapshot reflects what WebKit rendered in that view at capture time; it does not turn blocked or unavailable content into loaded content.
7. Troubleshooting common problems
| Symptom | Likely cause | Fix |
|---|---|---|
| The completion handler returns no image | The operation failed, or the view was not in a useful state. | Inspect the error, make the view’s lifecycle and size explicit, and show a recoverable error state rather than force-unwrapping. |
| The image is blank or incomplete | The page has not rendered the content yet, or an asynchronous resource is still loading. | Capture after navigation, then wait for a page-specific readiness condition if needed. Remember that pending screen updates are not the same as all resources being ready. |
| The captured area is the wrong size | The configured rectangle uses unexpected coordinates, or the view has not completed layout. | Log webView.bounds, calculate the rectangle after layout, and test on the target device sizes. |
| The output is too small or too large | The output width or display scale does not match the intended use. | Set snapshotWidth deliberately and check the final pixel dimensions and memory use. |
| It only captures the visible portion | The capture rectangle covers the view region, and the API documentation does not promise arbitrary full-page capture. | Confirm the required scope on each supported platform. If the goal is a page-length document, assess the PDF API. |
| Saving fails | Image encoding returned nil, or the destination is unavailable. | Check pngData() or JPEG encoding for failure, write atomically, and use a location appropriate to the file’s lifetime. |
| The screenshot changes between runs | Dynamic content, animations, ads, personalization, or live data changed. | Use a controlled page or test fixture where possible; wait for the same readiness condition and disable page animations in content you own. |
8. Performance, reliability, and cost
A local WebKit snapshot has no per-request screenshot service charge, but the app still pays in time, memory, storage, and network use to load and encode the page. Large regions and high output widths can increase image memory and file size. Capture only when needed, release references to images when finished, and choose PNG or JPEG based on your content and downstream use. PNG is lossless; JPEG can be smaller for photographic content but is lossy.
Reliability depends on the page as well as your capture code. A successful navigation does not ensure that all dynamic content has settled, and a successful snapshot does not mean the page was complete. Handle both navigation and snapshot failures, put bounds on any readiness wait, and make retries selective. Repeating an expensive page load immediately may not help if the underlying site is unavailable or blocking the embedded browser.
For server-side capture, batch processing, or screenshots used by a backend rather than a device UI, a screenshot API can avoid managing a browser process in your own infrastructure. Compare the output and billing behavior you need, then account for API requests and your own storage. The next option is ScreenshotNeo, the publisher’s website screenshot API and MCP server.
9. Or skip the browser setup
For a server-side screenshot, ScreenshotNeo takes one GET request with a URL and returns an image or PDF. It is a website screenshot API and MCP server from ScreenshotNeo. The API accepts options for formats such as PNG, JPEG, or WebP, among other capture settings. See the ScreenshotNeo API documentation for the request parameters.

curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Use the same endpoint from Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Or from Node.js:
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
The code examples use https://stripe.com as the target; replace it with the page you need to capture. Keep your API key on a trusted server rather than exposing it in a public client. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before the shot; each of those steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan, and yearly billing gives two months free. Sign up for 1,000 free screenshots a month with no card.
10. Frequently asked questions
Does WKWebView take a screenshot of Safari or the whole screen?
No. This method asks a specific WKWebView for an image of its rendered contents. It is not a capture of Safari or the full device display.
Does takeSnapshot wait for every image and animation?
No such general guarantee is established by the documented snapshot configuration. Wait for content that matters to your app, and use a page-specific readiness check when you control the page.
Can I get an NSImage on iPhone?
The documented result type is platform-specific: UIKit-family platforms use UIImage; macOS uses NSImage. Handle the type appropriate to the target.
Should I use an image or PDF?
Use takeSnapshot for a raster image of the configured web view region. Use pdf(configuration:) when your intended output is PDF data, then validate the result against your document needs.


