Skip to content

Instantly share code, notes, and snippets.

@kovs705
Forked from helje5/SVGWebView.swift
Last active October 4, 2023 14:36
Show Gist options
  • Save kovs705/2e034e9feebc0acbf8eea2b962986018 to your computer and use it in GitHub Desktop.
Save kovs705/2e034e9feebc0acbf8eea2b962986018 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.
// Modified by Kovs705 on 04.10.2023
// 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.
*/
// MARK: SVGWebView
public struct SVGWebView: View {
private let svg: String
public init(svg: String) {
print("public init stuff")
self.svg = svg
}
public var body: some View {
WebView(html:
// "<div style=\"width: 100%; height: 100%;\">\(rewriteSVGSize(svg))</div>"
"<html><body><div style=\"width: 100%; height: 100%;\">\(rewriteSVGSize(svg))</div></body></html>"
)
}
/// 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)
}
// MARK: - WebView
private struct WebView : UIViewRepresentable {
@State private var isLoading = true
let html : String
let loadingView: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(style: .medium)
indicator.translatesAutoresizingMaskIntoConstraints = false
return indicator
}()
private func makeWebView() -> WKWebView {
let prefs = WKPreferences()
let webPagePref = WKWebpagePreferences()
let userContentController = WKUserContentController()
prefs.javaScriptCanOpenWindowsAutomatically = false
let config = WKWebViewConfiguration()
config.preferences = prefs
config.defaultWebpagePreferences = webPagePref
config.allowsAirPlayForMediaPlayback = false
config.userContentController = userContentController
let script = WKUserScript(source: Constants.source, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
userContentController.addUserScript(script)
let webView = WKWebView(frame: .zero, configuration: config)
let coordinator = makeCoordinator()
webView.navigationDelegate = coordinator
webView.scrollView.isScrollEnabled = false
webView.loadHTMLString(html, baseURL: nil)
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)
}
func makeUIView(context: Context) -> WKWebView {
let webView = makeWebView()
webView.addSubview(loadingView)
loadingView.centerXAnchor.constraint(equalTo: webView.centerXAnchor).isActive = true
loadingView.centerYAnchor.constraint(equalTo: webView.centerYAnchor).isActive = true
if isLoading {
loadingView.startAnimating()
loadingView.isHidden = false
} else {
loadingView.stopAnimating()
loadingView.isHidden = true
}
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
if isLoading {
loadingView.startAnimating()
loadingView.isHidden = false
} else {
loadingView.stopAnimating()
loadingView.isHidden = true
}
updateWebView(webView, context: context)
}
func makeCoordinator() -> Coordinator {
Coordinator(isLoading: $isLoading)
}
}
class Coordinator: NSObject, WKNavigationDelegate {
@Binding var isLoading: Bool
init(isLoading: Binding<Bool>) {
_isLoading = isLoading
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
isLoading = false
}
}
}
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)
}
}
@kovs705
Copy link
Author

kovs705 commented Oct 4, 2023

I modified original SVGWebView by helje5 with loading indicator before the svg appears + disabled zooming since I saw the comment by misteu

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