{"id":"9e029c0a-220d-4669-998c-0e57643a5743","shortId":"8xhQAm","kind":"skill","title":"Swiftui Webkit","tagline":"Swift Ios Skills skill by Dpearson2699","description":"# SwiftUI WebKit\n\nEmbed and manage web content in SwiftUI using the native WebKit-for-SwiftUI APIs introduced for iOS 26, iPadOS 26, macOS 26, and visionOS 26. Use this skill when the app needs an integrated web surface, app-owned HTML content, JavaScript-backed page interaction, or custom navigation policy control.\n\n## Contents\n\n- [Choose the Right Web Container](#choose-the-right-web-container)\n- [Displaying Web Content](#displaying-web-content)\n- [Loading and Observing with WebPage](#loading-and-observing-with-webpage)\n- [Navigation Policies](#navigation-policies)\n- [JavaScript Integration](#javascript-integration)\n- [Local Content and Custom URL Schemes](#local-content-and-custom-url-schemes)\n- [WebView Customization](#webview-customization)\n- [Common Mistakes](#common-mistakes)\n- [Review Checklist](#review-checklist)\n- [References](#references)\n\n## Choose the Right Web Container\n\nUse the narrowest tool that matches the job.\n\n| Need | Default choice |\n|---|---|\n| Embedded app-owned web content in SwiftUI | `WebView` + `WebPage` |\n| Simple external site presentation with Safari behavior | `SFSafariViewController` |\n| OAuth or third-party sign-in | `ASWebAuthenticationSession` |\n| Back-deploy below iOS 26 or use missing legacy-only WebKit features | `WKWebView` fallback |\n\nPrefer `WebView` and `WebPage` for modern SwiftUI apps targeting iOS 26+. Apple’s WWDC25 guidance explicitly recommends migrating SwiftUI apps away from UIKit/AppKit WebKit wrappers when possible.\n\nDo not use embedded web views for OAuth. That stays an `ASWebAuthenticationSession` flow.\n\n## Displaying Web Content\n\nUse the simple `WebView(url:)` form when the app only needs to render a URL and SwiftUI state drives navigation.\n\n```swift\nimport SwiftUI\nimport WebKit\n\nstruct ArticleView: View {\n    let url: URL\n\n    var body: some View {\n        WebView(url: url)\n    }\n}\n```\n\nCreate a `WebPage` when the app needs to load requests directly, observe state, call JavaScript, or customize navigation behavior.\n\n```swift\n@Observable\n@MainActor\nfinal class ArticleModel {\n    let page = WebPage()\n\n    func load(_ url: URL) async throws {\n        for try await _ in page.load(URLRequest(url: url)) {\n        }\n    }\n}\n\nstruct ArticleDetailView: View {\n    @State private var model = ArticleModel()\n    let url: URL\n\n    var body: some View {\n        WebView(model.page)\n            .task {\n                try? await model.load(url)\n            }\n    }\n}\n```\n\nSee [references/loading-and-observation.md](references/loading-and-observation.md) for full examples.\n\n## Loading and Observing with WebPage\n\n`WebPage` is an `@MainActor` observable type. Use it when you need page state in SwiftUI.\n\nCommon loading entry points:\n- `load(URLRequest)`\n- `load(URL)`\n- `load(html:baseURL:)`\n- `load(_:mimeType:characterEncoding:baseURL:)`\n\nCommon observable properties:\n- `title`\n- `url`\n- `isLoading`\n- `estimatedProgress`\n- `currentNavigationEvent`\n- `backForwardList`\n\n```swift\nstruct ReaderView: View {\n    @State private var page = WebPage()\n\n    var body: some View {\n        WebView(page)\n            .navigationTitle(page.title ?? \"Loading\")\n            .overlay {\n                if page.isLoading {\n                    ProgressView(value: page.estimatedProgress)\n                }\n            }\n            .task {\n                do {\n                    for try await _ in page.load(URLRequest(url: URL(string: \"https://example.com\")!)) {\n                    }\n                } catch {\n                    // Handle load failure.\n                }\n            }\n    }\n}\n```\n\nWhen you need to react to every navigation, observe the navigation sequence rather than only checking a single property.\n\n```swift\nTask {\n    for await event in page.navigations {\n        // Handle finish, redirect, or failure events.\n    }\n}\n```\n\nSee [references/loading-and-observation.md](references/loading-and-observation.md) for stronger patterns and the load-sequence examples.\n\n## Navigation Policies\n\nUse `WebPage.NavigationDeciding` to allow, cancel, or customize navigations based on the request or response.\n\nTypical uses:\n- keep app-owned domains inside the embedded web view\n- cancel external domains and hand them off with `openURL`\n- intercept special callback URLs\n- tune `NavigationPreferences`\n\n```swift\n@MainActor\nfinal class ArticleNavigationDecider: WebPage.NavigationDeciding {\n    var urlToOpenExternally: URL?\n\n    func decidePolicy(\n        for action: WebPage.NavigationAction,\n        preferences: inout WebPage.NavigationPreferences\n    ) async -> WKNavigationActionPolicy {\n        guard let url = action.request.url else { return .allow }\n\n        if url.host == \"example.com\" {\n            return .allow\n        }\n\n        urlToOpenExternally = url\n        return .cancel\n    }\n}\n```\n\nKeep app-level deep-link routing in the navigation skill. This skill owns navigation that happens inside embedded web content.\n\nSee [references/navigation-and-javascript.md](references/navigation-and-javascript.md) for complete patterns.\n\n## JavaScript Integration\n\nUse `callJavaScript(_:arguments:in:contentWorld:)` to evaluate JavaScript functions against the page.\n\n```swift\nlet script = \"\"\"\nconst headings = [...document.querySelectorAll('h1, h2')];\nreturn headings.map(node => ({\n    id: node.id,\n    text: node.textContent?.trim()\n}));\n\"\"\"\n\nlet result = try await page.callJavaScript(script)\nlet headings = result as? [[String: Any]] ?? []\n```\n\nYou can pass values through the `arguments` dictionary and cast the returned `Any` into the Swift type you actually need.\n\n```swift\nlet result = try await page.callJavaScript(\n    \"return document.getElementById(sectionID)?.getBoundingClientRect().top ?? null;\",\n    arguments: [\"sectionID\": selectedSectionID]\n)\n```\n\nImportant boundary: the native SwiftUI WebKit API clearly supports Swift-to-JavaScript calls, but it does not expose an obvious direct replacement for `WKScriptMessageHandler`. If you need coarse JS-to-native signaling, a custom navigation or callback-URL pattern can work, but document it as a workaround pattern, not a guaranteed one-to-one replacement.\n\nSee [references/navigation-and-javascript.md](references/navigation-and-javascript.md).\n\n## Local Content and Custom URL Schemes\n\nUse `WebPage.Configuration` and `URLSchemeHandler` when the app needs bundled HTML, offline documents, or app-provided resources under a custom scheme.\n\n```swift\nvar configuration = WebPage.Configuration()\nconfiguration.urlSchemeHandlers[URLScheme(\"docs\")!] = DocsSchemeHandler(bundle: .main)\n\nlet page = WebPage(configuration: configuration)\nfor try await _ in page.load(URL(string: \"docs://article/welcome\")!) {\n}\n\n```\n\nUse this for:\n- bundled documentation or article content\n- offline HTML/CSS/JS assets\n- app-owned resource loading under a custom scheme\n\nDo not overuse custom schemes for normal remote content. Prefer standard HTTPS for server-hosted pages.\n\nSee [references/local-content-and-custom-schemes.md](references/local-content-and-custom-schemes.md).\n\n## WebView Customization\n\nUse WebView modifiers to match the intended browsing experience.\n\nUseful modifiers and related APIs:\n- `webViewBackForwardNavigationGestures(_:)`\n- `findNavigator(isPresented:)`\n- `webViewScrollPosition(_:)`\n- `webViewOnScrollGeometryChange(...)`\n\nApply them only when the user experience needs them.\n\n- Enable back/forward gestures when people are likely to visit multiple pages.\n- Add Find in Page when the content is document-like.\n- Sync scroll position only when the app has a sidebar, table of contents, or other explicit navigation affordance.\n\nApple’s HIG also applies here: support back/forward navigation when appropriate, but do not turn an app web view into a general-purpose browser.\n\n## Common Mistakes\n\n- Using `WKWebView` wrappers by default in an iOS 26+ SwiftUI app instead of starting with `WebView` and `WebPage`\n- Using embedded web views for OAuth instead of `ASWebAuthenticationSession`\n- Reaching for `WebPage` only after building a plain `WebView(url:)` path that now needs state, JS, or navigation control\n- Treating `callJavaScript` as a direct replacement for `WKScriptMessageHandler`\n- Keeping all links inside the app when external domains should open outside the embedded surface\n- Building a browser-style app shell around WebView instead of a focused embedded experience\n- Using custom URL schemes for content that should just load over HTTPS\n- Forgetting that `WebPage` is main-actor-isolated\n\n## Review Checklist\n\n- [ ] `WebView` and `WebPage` are the default path for iOS 26+ SwiftUI web content\n- [ ] `ASWebAuthenticationSession` is used for auth flows instead of embedded web views\n- [ ] `WebPage` is used whenever the app needs state observation, JS calls, or policy control\n- [ ] Navigation policies only intercept the URLs the app actually owns or needs to reroute\n- [ ] External domains open externally when appropriate\n- [ ] JavaScript return values are cast defensively to concrete Swift types\n- [ ] Custom URL schemes are used only for real app-owned resources\n- [ ] Back/forward gestures or controls are enabled when multi-page browsing is expected\n- [ ] The web experience adds focused native value instead of behaving like a thin browser shell\n- [ ] Fallback to `WKWebView` is justified by deployment target or missing API needs\n\n## References\n\n- Loading and observation: [references/loading-and-observation.md](references/loading-and-observation.md)\n- Navigation and JavaScript: [references/navigation-and-javascript.md](references/navigation-and-javascript.md)\n- Local content and custom schemes: [references/local-content-and-custom-schemes.md](references/local-content-and-custom-schemes.md)\n- Migration and fallbacks: [references/migration-and-fallbacks.md](references/migration-and-fallbacks.md)","tags":["swiftui","webkit","swift","ios","skills","dpearson2699"],"capabilities":["skill","source-dpearson2699","category-swift-ios-skills"],"categories":["swift-ios-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/dpearson2699/swift-ios-skills/swiftui-webkit","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"install_from":"skills.sh"}},"qualityScore":"0.300","qualityRationale":"deterministic score 0.30 from registry signals: · indexed on skills.sh · published under dpearson2699/swift-ios-skills","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill:v1","enrichmentVersion":1,"enrichedAt":"2026-04-22T03:40:42.123Z","embedding":null,"createdAt":"2026-04-18T20:38:52.653Z","updatedAt":"2026-04-22T03:40:42.123Z","lastSeenAt":"2026-04-22T03:40:42.123Z","tsv":"'26':29,31,33,36,181,202,911,1018 'action':526 'action.request.url':536 'actor':1005 'actual':637,1055 'add':847,1105 'afford':875 'allow':476,539,544 'also':879 'api':25,660,821,1127 'app':42,49,151,199,211,243,278,491,551,728,736,778,864,892,913,962,977,1038,1054,1086 'app-level':550 'app-own':48,150,490,777,1085 'app-provid':735 'appl':203,876 'appli':827,880 'appropri':886,1066 'argument':581,625,651 'around':979 'articl':772 'article/welcome':765 'articledetailview':316 'articlemodel':297,322 'articlenavigationdecid':518 'articleview':261 'asset':776 'aswebauthenticationsess':175,230,929,1022 'async':305,531 'auth':1026 'await':309,334,415,449,610,643,760 'away':212 'back':55,177 'back-deploy':176 'back/forward':837,883,1089 'backforwardlist':386 'base':481 'baseurl':373,377 'behav':1111 'behavior':165,291 'bodi':267,327,397 'boundari':655 'brows':815,1099 'browser':900,975,1115 'browser-styl':974 'build':935,972 'bundl':730,751,769 'call':286,667,1043 'callback':510,693 'callback-url':692 'calljavascript':580,950 'cancel':477,499,548 'cast':628,1071 'catch':423 'category-swift-ios-skills' 'characterencod':376 'check':442 'checklist':127,130,1008 'choic':148 'choos':64,70,133 'choose-the-right-web-contain':69 'class':296,517 'clear':661 'coars':682 'common':121,124,363,378,901 'common-mistak':123 'complet':575 'concret':1074 'configur':745,756,757 'configuration.urlschemehandlers':747 'const':594 'contain':68,74,137 'content':15,52,63,77,81,104,111,154,234,570,717,773,794,853,870,992,1021,1141 'contentworld':583 'control':62,948,1046,1092 'creat':273 'currentnavigationev':385 'custom':59,106,113,117,120,289,479,689,719,741,784,789,807,988,1077,1143 'decidepolici':524 'deep':554 'deep-link':553 'default':147,907,1014 'defens':1072 'deploy':178,1123 'dictionari':626 'direct':283,675,953 'display':75,79,232 'displaying-web-cont':78 'doc':749 'docsschemehandl':750 'document':699,733,770,856 'document-lik':855 'document.getelementbyid':646 'document.queryselectorall':596 'domain':493,501,965,1062 'dpearson2699':8 'drive':253 'els':537 'emb':11 'embed':149,222,496,568,922,970,985,1030 'enabl':836,1094 'entri':365 'estimatedprogress':384 'evalu':585 'event':450,458 'everi':433 'exampl':342,470 'example.com':422,542 'expect':1101 'experi':816,833,986,1104 'explicit':207,873 'expos':672 'extern':160,500,964,1061,1064 'failur':426,457 'fallback':191,1117,1149 'featur':189 'final':295,516 'find':848 'findnavig':823 'finish':454 'flow':231,1027 'focus':984,1106 'forget':999 'form':240 'full':341 'func':301,523 'function':587 'general':898 'general-purpos':897 'gestur':838,1090 'getboundingclientrect':648 'guarante':707 'guard':533 'guidanc':206 'h1':597 'h2':598 'hand':503 'handl':424,453 'happen':566 'head':595,614 'headings.map':600 'hig':878 'host':801 'html':51,372,731 'html/css/js':775 'https':797,998 'id':602 'import':256,258,654 'inout':529 'insid':494,567,960 'instead':914,927,981,1028,1109 'integr':45,99,102,578 'intend':814 'interact':57 'intercept':508,1050 'introduc':26 'io':4,28,180,201,910,1017 'ipado':30 'isload':383 'isol':1006 'ispres':824 'javascript':54,98,101,287,577,586,666,1067,1137 'javascript-back':53 'javascript-integr':100 'job':145 'js':684,945,1042 'js-to-nat':683 'justifi':1121 'keep':489,549,957 'legaci':186 'legacy-on':185 'let':263,298,323,534,592,607,613,640,753 'level':552 'like':842,857,1112 'link':555,959 'load':82,88,281,302,343,364,367,369,371,374,404,425,468,781,996,1130 'load-sequ':467 'loading-and-observing-with-webpag':87 'local':103,110,716,1140 'local-content-and-custom-url-schem':109 'maco':32 'main':752,1004 'main-actor-isol':1003 'mainactor':294,351,515 'manag':13 'match':143,812 'migrat':209,1147 'mimetyp':375 'miss':184,1126 'mistak':122,125,902 'model':321 'model.load':335 'model.page':331 'modern':197 'modifi':810,818 'multi':1097 'multi-pag':1096 'multipl':845 'narrowest':140 'nativ':20,657,686,1107 'navig':60,93,96,254,290,434,437,471,480,559,564,690,874,884,947,1047,1135 'navigation-polici':95 'navigationprefer':513 'navigationtitl':402 'need':43,146,245,279,358,429,638,681,729,834,943,1039,1058,1128 'node':601 'node.id':603 'node.textcontent':605 'normal':792 'null':650 'oauth':167,226,926 'observ':84,90,284,293,345,352,379,435,1041,1132 'obvious':674 'offlin':732,774 'one':709,711 'one-to-on':708 'open':967,1063 'openurl':507 'outsid':968 'overlay':405 'overus':788 'own':50,152,492,563,779,1056,1087 'page':56,299,359,394,401,590,754,802,846,850,1098 'page.calljavascript':611,644 'page.estimatedprogress':410 'page.isloading':407 'page.load':311,417,762 'page.navigations':452 'page.title':403 'parti':171 'pass':621 'path':940,1015 'pattern':464,576,695,704 'peopl':840 'plain':937 'point':366 'polici':61,94,97,472,1045,1048 'posit':860 'possibl':218 'prefer':192,528,795 'present':162 'privat':319,392 'progressview':408 'properti':380,445 'provid':737 'purpos':899 'rather':439 'reach':930 'react':431 'readerview':389 'real':1084 'recommend':208 'redirect':455 'refer':131,132,1129 'references/loading-and-observation.md':338,339,460,461,1133,1134 'references/local-content-and-custom-schemes.md':804,805,1145,1146 'references/migration-and-fallbacks.md':1150,1151 'references/navigation-and-javascript.md':572,573,714,715,1138,1139 'relat':820 'remot':793 'render':247 'replac':676,712,954 'request':282,484 'rerout':1060 'resourc':738,780,1088 'respons':486 'result':608,615,641 'return':538,543,547,599,630,645,1068 'review':126,129,1007 'review-checklist':128 'right':66,72,135 'rout':556 'safari':164 'scheme':108,115,721,742,785,790,990,1079,1144 'script':593,612 'scroll':859 'sectionid':647,652 'see':337,459,571,713,803 'selectedsectionid':653 'sequenc':438,469 'server':800 'server-host':799 'sfsafariviewcontrol':166 'shell':978,1116 'sidebar':867 'sign':173 'sign-in':172 'signal':687 'simpl':159,237 'singl':444 'site':161 'skill':5,6,39,560,562 'source-dpearson2699' 'special':509 'standard':796 'start':916 'state':252,285,318,360,391,944,1040 'stay':228 'string':421,617,764 'stronger':463 'struct':260,315,388 'style':976 'support':662,882 'surfac':47,971 'swift':3,255,292,387,446,514,591,634,639,664,743,1075 'swift-to-javascript':663 'swiftui':1,9,17,24,156,198,210,251,257,362,658,912,1019 'sync':858 'tabl':868 'target':200,1124 'task':332,411,447 'text':604 'thin':1114 'third':170 'third-parti':169 'throw':306 'titl':381 'tool':141 'top':649 'treat':949 'tri':308,333,414,609,642,759 'trim':606 'tune':512 'turn':890 'type':353,635,1076 'typic':487 'uikit/appkit':214 'url':107,114,239,249,264,265,271,272,303,304,313,314,324,325,336,370,382,419,420,511,522,535,546,694,720,763,939,989,1052,1078 'url.host':541 'urlrequest':312,368,418 'urlschem':748 'urlschemehandl':725 'urltoopenextern':521,545 'use':18,37,138,183,221,235,354,473,488,579,722,766,808,817,903,921,987,1024,1035,1081 'user':832 'valu':409,622,1069,1108 'var':266,320,326,393,396,520,744 'view':224,262,269,317,329,390,399,498,894,924,1032 'visiono':35 'visit':844 'web':14,46,67,73,76,80,136,153,223,233,497,569,893,923,1020,1031,1103 'webkit':2,10,22,188,215,259,659 'webkit-for-swiftui':21 'webpag':86,92,158,195,275,300,347,348,395,755,920,932,1001,1011,1033 'webpage.configuration':723,746 'webpage.navigationaction':527 'webpage.navigationdeciding':474,519 'webpage.navigationpreferences':530 'webview':116,119,157,193,238,270,330,400,806,809,918,938,980,1009 'webview-custom':118 'webviewbackforwardnavigationgestur':822 'webviewonscrollgeometrychang':826 'webviewscrollposit':825 'whenev':1036 'wknavigationactionpolici':532 'wkscriptmessagehandl':678,956 'wkwebview':190,904,1119 'work':697 'workaround':703 'wrapper':216,905 'wwdc25':205","prices":[{"id":"70e78218-7899-4b4c-a228-fa17341656a8","listingId":"9e029c0a-220d-4669-998c-0e57643a5743","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"dpearson2699","category":"swift-ios-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T20:38:52.653Z"}],"sources":[{"listingId":"9e029c0a-220d-4669-998c-0e57643a5743","source":"github","sourceId":"dpearson2699/swift-ios-skills/swiftui-webkit","sourceUrl":"https://github.com/dpearson2699/swift-ios-skills/tree/main/skills/swiftui-webkit","isPrimary":false,"firstSeenAt":"2026-04-18T22:01:28.843Z","lastSeenAt":"2026-04-22T00:53:45.744Z"},{"listingId":"9e029c0a-220d-4669-998c-0e57643a5743","source":"skills_sh","sourceId":"dpearson2699/swift-ios-skills/swiftui-webkit","sourceUrl":"https://skills.sh/dpearson2699/swift-ios-skills/swiftui-webkit","isPrimary":true,"firstSeenAt":"2026-04-18T20:38:52.653Z","lastSeenAt":"2026-04-22T03:40:42.123Z"}],"details":{"listingId":"9e029c0a-220d-4669-998c-0e57643a5743","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"dpearson2699","slug":"swiftui-webkit","source":"skills_sh","category":"swift-ios-skills","skills_sh_url":"https://skills.sh/dpearson2699/swift-ios-skills/swiftui-webkit"},"updatedAt":"2026-04-22T03:40:42.123Z"}}