Skip to content

Instantly share code, notes, and snippets.

@helje5
Created May 10, 2021 12:54
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save helje5/941f076a2f73a6bad0ba87eb4f67f229 to your computer and use it in GitHub Desktop.
A SwiftUI View to display SVGs using WKWebView
// Created by Helge Heß on 06.04.21.
// Also available as a package: https://github.com/ZeeZide/SVGWebView
import SwiftUI
import WebKit
/**
* Display an SVG using a `WKWebView`.
*
* Used by [SVG Shaper for SwiftUI](https://zeezide.de/en/products/svgshaper/)
* to display the SVG preview in the sidebar.
*
* This patches the XML of the SVG to fit the WebView contents.
*
* IMPORTANT: On macOS `WKWebView` requires the "outgoing internet connection"
* entitlement to operate, otherwise it'll show up blank.
* Xcode Previews do not work quite right with the iOS variant, best to test in
* a real simulator.
*/
public struct SVGWebView: View {
private let svg: String
public init(svg: String) { self.svg = svg }
public var body: some View {
WebView(html:
"<div style=\"width: 100%; height: 100%;\">\(rewriteSVGSize(svg))</div>"
)
}
/// A hacky way to patch the size in the SVG root tag.
private func rewriteSVGSize(_ string: String) -> String {
guard let startRange = string.range(of: "<svg") else { return string }
let remainder = startRange.upperBound..<string.endIndex
guard let endRange = string.range(of: ">", range: remainder) else {
return string
}
let tagRange = startRange.lowerBound..<endRange.upperBound
let oldTag = string[tagRange]
var attrs : [ String : String ] = {
final class Handler: NSObject, XMLParserDelegate {
var attrs : [ String : String ]?
func parser(_ parser: XMLParser, didStartElement: String,
namespaceURI: String?, qualifiedName: String?,
attributes: [ String : String ])
{
self.attrs = attributes
}
}
let parser = XMLParser(data: Data((string[tagRange] + "</svg>").utf8))
let handler = Handler()
parser.delegate = handler
guard parser.parse() else { return [:] }
return handler.attrs ?? [:]
}()
if attrs["viewBox"] == nil &&
(attrs["width"] != nil || attrs["height"] != nil)
{ // convert to viewBox
let w = attrs.removeValue(forKey: "width") ?? "100%"
let h = attrs.removeValue(forKey: "height") ?? "100%"
let x = attrs.removeValue(forKey: "x") ?? "0"
let y = attrs.removeValue(forKey: "y") ?? "0"
attrs["viewBox"] = "\(x) \(y) \(w) \(h)"
}
attrs.removeValue(forKey: "x")
attrs.removeValue(forKey: "y")
attrs["width"] = "100%"
attrs["height"] = "100%"
func renderTag(_ tag: String, attributes: [ String : String ]) -> String {
var ms = "<\(tag)"
for ( key, value ) in attributes {
ms += " \(key)=\""
ms += value
.replacingOccurrences(of: "&", with: "&amp;")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
.replacingOccurrences(of: "'", with: "&apos;")
.replacingOccurrences(of: "\"", with: "&quot;")
ms += "\""
}
ms += ">"
return ms
}
let newTag = renderTag("svg", attributes: attrs)
return newTag == oldTag
? string
: string.replacingCharacters(in: tagRange, with: newTag)
}
#if os(macOS)
typealias UXViewRepresentable = NSViewRepresentable
#else
typealias UXViewRepresentable = UIViewRepresentable
#endif
private struct WebView : UXViewRepresentable {
let html : String
private func makeWebView() -> WKWebView {
let prefs = WKPreferences()
#if os(macOS)
if #available(macOS 10.5, *) {} else { prefs.javaEnabled = false }
#endif
if #available(macOS 11, *) {} else { prefs.javaScriptEnabled = false }
prefs.javaScriptCanOpenWindowsAutomatically = false
let config = WKWebViewConfiguration()
config.preferences = prefs
config.allowsAirPlayForMediaPlayback = false
if #available(macOS 10.5, *) {
let pagePrefs : WKWebpagePreferences = {
let prefs = WKWebpagePreferences()
prefs.preferredContentMode = .desktop
if #available(macOS 11, *) {
prefs.allowsContentJavaScript = false
}
return prefs
}()
config.defaultWebpagePreferences = pagePrefs
}
let webView = WKWebView(frame: .zero, configuration: config)
#if !os(macOS)
webView.scrollView.isScrollEnabled = false
#endif
webView.loadHTMLString(html, baseURL: nil)
// Sometimes necessary to make things show up initially. No idea why.
DispatchQueue.main.async {
let old = webView.frame
webView.frame = .zero
webView.frame = old
}
return webView
}
private func updateWebView(_ webView: WKWebView, context: Context) {
webView.loadHTMLString(html, baseURL: nil)
}
#if os(macOS)
func makeNSView(context: Context) -> WKWebView {
return makeWebView()
}
func updateNSView(_ webView: WKWebView, context: Context) {
updateWebView(webView, context: context)
}
#else // iOS etc
func makeUIView(context: Context) -> WKWebView {
return makeWebView()
}
func updateUIView(_ webView: WKWebView, context: Context) {
updateWebView(webView, context: context)
}
#endif
}
}
struct SVGWebView_Previews : PreviewProvider {
static var previews: some View {
SVGWebView(svg:
"""
<svg viewBox="0 0 100 100">
<rect x="10" y="10" width="80" height="80"
fill="gold" stroke="blue" stroke-width="4" />
</svg>
"""
)
.frame(width: 300, height: 200)
SVGWebView(svg:
"""
<svg width="120" height="120" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="Gradient1">
<stop offset="0%" stop-color="red"/>
<stop offset="50%" stop-color="black" stop-opacity="0"/>
<stop offset="100%" stop-color="blue"/>
</linearGradient>
</defs>
<rect x="10" y="10" rx="15" ry="15" width="100" height="100"
fill="url(#Gradient1)" />
</svg>
""")
.frame(width: 200, height: 200)
}
}
@helje5
Copy link
Author

helje5 commented May 10, 2021

On macOS, make sure to enable the "Outgoing Internet Connection" (Client)" entitlement in the sandbox settings, WKWebView requires this even if no outgoing Internet access is done.

Example usage in SVG Shaper for SwiftUI (it is the View displaying the SVG in the upper left):
SVG SHaper Screenshot

Note: SVG Shaper is for converting SVGs to SwiftUI source code (which then gets compiled). SVGWebView is for displaying SVG resources (e.g. loaded from a bundle or the web) at runtime. They serve different purposes.

@helje5
Copy link
Author

helje5 commented May 10, 2021

This is intended as a no-dependency option, to avoid linking a 3rd party SVG framework. If that's no problem, there are nice options for the latter:

Copy link

ghost commented Jan 27, 2022

Finally !!!! been searching for a mac solution for a week

@Enie
Copy link

Enie commented Jul 27, 2023

On macOS, make sure to enable the "Outgoing Internet Connection" (Client)" entitlement in the sandbox settings, WKWebView requires this even if no outgoing Internet access is done.

I sat here for one hour wondering why I don't see anything rendered in my macOS WebView. Thanks for the hint!

@helje5
Copy link
Author

helje5 commented Jul 27, 2023

Yeah, that is a very weird bug in WebKit.

@misteu
Copy link

misteu commented Sep 7, 2023

Thanks, seems to work so far!

I tried SwiftDraw before (see https://github.com/swhitty/SwiftDraw). It rasters nicely and fast without using any webview but it's a really huge library :/

Some things maybe worth mentioning to anyone trying to use the solution above, i.e. SVGWebView:

  • make sure to not use to many of these on one view or it becomes incredibly slow
  • Consider using a Lazy views of SwiftUI
  • at least on iOS you can zoom into the webview (or did I accidentally removed some code?), here is a small script you can inject to disable zooming: https://stackoverflow.com/a/41741125/1898677
  • You most probably want to have .clear background. This can be achieved by setting
webView.isOpaque = false
webView.backgroundColor = .clear

Still unsure if I should use this or a similar solution in production. In general it is seems either rather laggy (in a VStack) or it's a little bumpy when used in a LazyVStack (because it seems to reload to often). Maybe the latter can be solved by somehow cache stuff, e.g. making snapshots or something.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment