-
-
Save helje5/941f076a2f73a6bad0ba87eb4f67f229 to your computer and use it in GitHub Desktop.
// 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: "&") | |
.replacingOccurrences(of: "<", with: "<") | |
.replacingOccurrences(of: ">", with: ">") | |
.replacingOccurrences(of: "'", with: "'") | |
.replacingOccurrences(of: "\"", with: """) | |
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) | |
} | |
} |
Finally !!!! been searching for a mac solution for a week
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!
Yeah, that is a very weird bug in WebKit.
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.
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: