//
// SVGWebView.swift
// SVG Shaper for SwiftUI
//
// Created by Helge Heß on 06.04.21.
//
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:
"
\(rewriteSVGSize(svg))
"
)
}
/// A hacky way to patch the size in the SVG root tag.
private func rewriteSVGSize(_ string: String) -> String {
guard let startRange = string.range(of: "", range: remainder) else {
return string
}
let tagRange = startRange.lowerBound..").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:
"""
"""
)
.frame(width: 300, height: 200)
SVGWebView(svg:
"""
""")
.frame(width: 200, height: 200)
}
}