├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Images ├── preview_ios.gif └── preview_macos.gif ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── SwiftUIWebView │ └── SwiftUIWebView.swift └── SwiftUIWebView.podspec /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Images/preview_ios.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globulus/swiftui-webview/0c680af957f880dc2e4b66a56a7da2187255f5f5/Images/preview_ios.gif -------------------------------------------------------------------------------- /Images/preview_macos.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globulus/swiftui-webview/0c680af957f880dc2e4b66a56a7da2187255f5f5/Images/preview_macos.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gordan Glavaš 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftUIWebView", 8 | platforms: [ 9 | .iOS(.v13), .macOS(.v10_15) 10 | ], 11 | products: [ 12 | .library( 13 | name: "SwiftUIWebView", 14 | targets: ["SwiftUIWebView"]), 15 | ], 16 | dependencies: [ 17 | ], 18 | targets: [ 19 | .target( 20 | name: "SwiftUIWebView", 21 | dependencies: []) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stateful SwiftUI WebView for iOS and MacOS 2 | 3 | Fully functional, SwiftUI-ready WebView for iOS 13+ and MacOS 10.15+. Actions and state are both delivered via SwiftUI `@Binding`s, making it dead-easy to integrate into any existing SwiftUI View. 4 | 5 | ![Preview iOS](https://github.com/globulus/swiftui-webview/blob/main/Images/preview_ios.gif?raw=true) 6 | 7 | ![Preview macOS](https://github.com/globulus/swiftui-webview/blob/main/Images/preview_macos.gif?raw=true) 8 | 9 | ## Installation 10 | 11 | This component is distributed as a **Swift package**. Just add this URL to your package list: 12 | 13 | ```text 14 | https://github.com/globulus/swiftui-webview 15 | ``` 16 | 17 | You can also use **CocoaPods**: 18 | 19 | ```ruby 20 | pod 'SwiftUIWebView', '~> 1.0.8' 21 | ``` 22 | 23 | ## Sample usage 24 | 25 | * Pass the **config** parameter to optionally set various web view properties: 26 | + `javaScriptEnabled` 27 | + `allowsBackForwardNavigationGestures` 28 | + `allowsInlineMediaPlayback` 29 | + `mediaTypesRequiringUserActionForPlayback` 30 | + `isScrollEnabled` 31 | + `isOpaque` 32 | + `backgroundColor` 33 | * The **action** binding is used to control the WebView - whichever action you want it to perform, just set the variable's value to it. Available actions: 34 | + `idle` - does nothing and can be used as the default value. 35 | + `load(URLRequest)` - loads the given request. 36 | + `loadHTML(String)` - loads custom HTML string. 37 | + `reload` 38 | + `goBack` 39 | + `goForward` 40 | + `evaluateJS(String, (Result) -> Void)` - evaluate any JavaScript command in the web view and get the result via the callback. 41 | * The **state** binding reports back the current state of the WebView. Available data: 42 | + `isLoading` - `true` if the WebView is currently loading a page. 43 | + `pageURL` - the URL of the currently loaded page, or `nil` if it can't be obtained. 44 | + `pageTitle` - the title of the currently loaded page, or `nil` if it can't be obtained. 45 | + `pageHTML` - the HTML code of the page content. Set `htmlInState: true` in `WebView` initializer to receive this update. 46 | + `error` - set if an error ocurred while loading the page, `nil` otherwise. 47 | + `canGoBack` 48 | + `canGoForward` 49 | * The optional **restrictedPages** array allows you to specify hosts which the web view won't load. 50 | * **htmlInState** dictates if the `state` update will contain `pageHTML`. This is disabled by default as it's a costly operation. 51 | * Optional **schemeHandlers** allow you to invoke a custom callback whenever the user navigates to a site with the given scheme. 52 | 53 | ```swift 54 | import SwiftUIWebView 55 | 56 | struct WebViewTest: View { 57 | @State private var action = WebViewAction.idle 58 | @State private var state = WebViewState.empty 59 | @State private var address = "https://www.google.com" 60 | 61 | var body: some View { 62 | VStack { 63 | titleView 64 | navigationToolbar 65 | errorView 66 | Divider() 67 | WebView(action: $action, 68 | state: $state, 69 | restrictedPages: ["apple.com"]) 70 | Spacer() 71 | } 72 | } 73 | 74 | private var titleView: some View { 75 | Text(String(format: "%@ - %@", state.pageTitle ?? "Load a page", state.pageURL ?? "No URL")) 76 | .font(.system(size: 24)) 77 | } 78 | 79 | private var navigationToolbar: some View { 80 | HStack(spacing: 10) { 81 | TextField("Address", text: $address) 82 | if state.isLoading { 83 | if #available(iOS 14, macOS 10.15, *) { 84 | ProgressView() 85 | .progressViewStyle(CircularProgressViewStyle()) 86 | } else { 87 | Text("Loading") 88 | } 89 | } 90 | Spacer() 91 | Button("Go") { 92 | if let url = URL(string: address) { 93 | action = .load(URLRequest(url: url)) 94 | } 95 | } 96 | Button(action: { 97 | action = .reload 98 | }) { 99 | Image(systemName: "arrow.counterclockwise") 100 | .imageScale(.large) 101 | } 102 | if state.canGoBack { 103 | Button(action: { 104 | action = .goBack 105 | }) { 106 | Image(systemName: "chevron.left") 107 | .imageScale(.large) 108 | } 109 | } 110 | if state.canGoForward { 111 | Button(action: { 112 | action = .goForward 113 | }) { 114 | Image(systemName: "chevron.right") 115 | .imageScale(.large) 116 | 117 | } 118 | } 119 | }.padding() 120 | } 121 | 122 | private var errorView: some View { 123 | Group { 124 | if let error = state.error { 125 | Text(error.localizedDescription) 126 | .foregroundColor(.red) 127 | } 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | ## Recipe 134 | 135 | For a more detailed description of the code, [visit this recipe](https://swiftuirecipes.com/blog/webview-in-swiftui). Check out [SwiftUIRecipes.com](https://swiftuirecipes.com) for more **SwiftUI recipes**! 136 | 137 | ## Changelog 138 | 139 | * 1.0.8 - Fixed links with `target="_blank"`. 140 | * 1.0.7 - Added `pageURL` state property. 141 | * 1.0.6 - Fixed bug related to `isScrollEnabled`. 142 | * 1.0.5 - Fixed bugs related to `canGoBack` and `canGoForward`, prevented multiple overriding actions happening at the same time. 143 | * 1.0.4 - Updated deprecated mediaPlaybackRequiresUserAction. 144 | * 1.0.3 - Added config, JS evaluation and scheme handlers. 145 | * 1.0.2 - Added site HTML as response. 146 | * 1.0.1 - Added suport for loading custom HTML. 147 | * 1.0.0 - Initial release. 148 | -------------------------------------------------------------------------------- /Sources/SwiftUIWebView/SwiftUIWebView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import WebKit 3 | import Foundation 4 | 5 | public enum WebViewAction: Equatable { 6 | case idle, 7 | load(URLRequest), 8 | loadHTML(String), 9 | reload, 10 | goBack, 11 | goForward, 12 | evaluateJS(String, (Result) -> Void) 13 | 14 | 15 | public static func == (lhs: WebViewAction, rhs: WebViewAction) -> Bool { 16 | if case .idle = lhs, 17 | case .idle = rhs { 18 | return true 19 | } 20 | if case let .load(requestLHS) = lhs, 21 | case let .load(requestRHS) = rhs { 22 | return requestLHS == requestRHS 23 | } 24 | if case let .loadHTML(htmlLHS) = lhs, 25 | case let .loadHTML(htmlRHS) = rhs { 26 | return htmlLHS == htmlRHS 27 | } 28 | if case .reload = lhs, 29 | case .reload = rhs { 30 | return true 31 | } 32 | if case .goBack = lhs, 33 | case .goBack = rhs { 34 | return true 35 | } 36 | if case .goForward = lhs, 37 | case .goForward = rhs { 38 | return true 39 | } 40 | if case let .evaluateJS(commandLHS, _) = lhs, 41 | case let .evaluateJS(commandRHS, _) = rhs { 42 | return commandLHS == commandRHS 43 | } 44 | return false 45 | } 46 | } 47 | 48 | public struct WebViewState: Equatable { 49 | public internal(set) var isLoading: Bool 50 | public internal(set) var pageURL: String? 51 | public internal(set) var pageTitle: String? 52 | public internal(set) var pageHTML: String? 53 | public internal(set) var error: Error? 54 | public internal(set) var canGoBack: Bool 55 | public internal(set) var canGoForward: Bool 56 | 57 | public static let empty = WebViewState(isLoading: false, 58 | pageURL: nil, 59 | pageTitle: nil, 60 | pageHTML: nil, 61 | error: nil, 62 | canGoBack: false, 63 | canGoForward: false) 64 | 65 | public static func == (lhs: WebViewState, rhs: WebViewState) -> Bool { 66 | lhs.isLoading == rhs.isLoading 67 | && lhs.pageURL == rhs.pageURL 68 | && lhs.pageTitle == rhs.pageTitle 69 | && lhs.pageHTML == rhs.pageHTML 70 | && lhs.error?.localizedDescription == rhs.error?.localizedDescription 71 | && lhs.canGoBack == rhs.canGoBack 72 | && lhs.canGoForward == rhs.canGoForward 73 | } 74 | } 75 | 76 | public class WebViewCoordinator: NSObject { 77 | private let webView: WebView 78 | var actionInProgress = false 79 | 80 | init(webView: WebView) { 81 | self.webView = webView 82 | } 83 | 84 | func setLoading(_ isLoading: Bool, 85 | canGoBack: Bool? = nil, 86 | canGoForward: Bool? = nil, 87 | error: Error? = nil) { 88 | var newState = webView.state 89 | newState.isLoading = isLoading 90 | if let canGoBack = canGoBack { 91 | newState.canGoBack = canGoBack 92 | } 93 | if let canGoForward = canGoForward { 94 | newState.canGoForward = canGoForward 95 | } 96 | if let error = error { 97 | newState.error = error 98 | } 99 | webView.state = newState 100 | webView.action = .idle 101 | actionInProgress = false 102 | } 103 | } 104 | 105 | extension WebViewCoordinator: WKNavigationDelegate { 106 | public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 107 | setLoading(false, 108 | canGoBack: webView.canGoBack, 109 | canGoForward: webView.canGoForward) 110 | 111 | webView.evaluateJavaScript("document.title") { (response, error) in 112 | if let title = response as? String { 113 | var newState = self.webView.state 114 | newState.pageTitle = title 115 | self.webView.state = newState 116 | } 117 | } 118 | 119 | webView.evaluateJavaScript("document.URL.toString()") { (response, error) in 120 | if let url = response as? String { 121 | var newState = self.webView.state 122 | newState.pageURL = url 123 | self.webView.state = newState 124 | } 125 | } 126 | 127 | if self.webView.htmlInState { 128 | webView.evaluateJavaScript("document.documentElement.outerHTML.toString()") { (response, error) in 129 | if let html = response as? String { 130 | var newState = self.webView.state 131 | newState.pageHTML = html 132 | self.webView.state = newState 133 | } 134 | } 135 | } 136 | } 137 | 138 | public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { 139 | setLoading(false) 140 | } 141 | 142 | public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { 143 | setLoading(false, error: error) 144 | } 145 | 146 | public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { 147 | setLoading(true) 148 | } 149 | 150 | public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { 151 | setLoading(true, 152 | canGoBack: webView.canGoBack, 153 | canGoForward: webView.canGoForward) 154 | } 155 | 156 | public func webView(_ webView: WKWebView, 157 | decidePolicyFor navigationAction: WKNavigationAction, 158 | decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { 159 | if let host = navigationAction.request.url?.host { 160 | if self.webView.restrictedPages?.first(where: { host.contains($0) }) != nil { 161 | decisionHandler(.cancel) 162 | setLoading(false) 163 | return 164 | } 165 | } 166 | if let url = navigationAction.request.url, 167 | let scheme = url.scheme, 168 | let schemeHandler = self.webView.schemeHandlers[scheme] { 169 | schemeHandler(url) 170 | decisionHandler(.cancel) 171 | return 172 | } 173 | decisionHandler(.allow) 174 | } 175 | } 176 | 177 | extension WebViewCoordinator: WKUIDelegate { 178 | public func webView(_ webView: WKWebView, 179 | createWebViewWith configuration: WKWebViewConfiguration, 180 | for navigationAction: WKNavigationAction, 181 | windowFeatures: WKWindowFeatures) -> WKWebView? { 182 | if navigationAction.targetFrame == nil { 183 | webView.load(navigationAction.request) 184 | } 185 | return nil 186 | } 187 | } 188 | 189 | public struct WebViewConfig { 190 | public static let `default` = WebViewConfig() 191 | 192 | public let javaScriptEnabled: Bool 193 | public let allowsBackForwardNavigationGestures: Bool 194 | public let allowsInlineMediaPlayback: Bool 195 | public let mediaTypesRequiringUserActionForPlayback: WKAudiovisualMediaTypes 196 | public let isScrollEnabled: Bool 197 | public let isOpaque: Bool 198 | public let backgroundColor: Color 199 | 200 | public init(javaScriptEnabled: Bool = true, 201 | allowsBackForwardNavigationGestures: Bool = true, 202 | allowsInlineMediaPlayback: Bool = true, 203 | mediaTypesRequiringUserActionForPlayback: WKAudiovisualMediaTypes = [], 204 | isScrollEnabled: Bool = true, 205 | isOpaque: Bool = true, 206 | backgroundColor: Color = .clear) { 207 | self.javaScriptEnabled = javaScriptEnabled 208 | self.allowsBackForwardNavigationGestures = allowsBackForwardNavigationGestures 209 | self.allowsInlineMediaPlayback = allowsInlineMediaPlayback 210 | self.mediaTypesRequiringUserActionForPlayback = mediaTypesRequiringUserActionForPlayback 211 | self.isScrollEnabled = isScrollEnabled 212 | self.isOpaque = isOpaque 213 | self.backgroundColor = backgroundColor 214 | } 215 | } 216 | 217 | #if os(iOS) 218 | public struct WebView: UIViewRepresentable { 219 | let config: WebViewConfig 220 | @Binding var action: WebViewAction 221 | @Binding var state: WebViewState 222 | let restrictedPages: [String]? 223 | let htmlInState: Bool 224 | let schemeHandlers: [String: (URL) -> Void] 225 | 226 | public init(config: WebViewConfig = .default, 227 | action: Binding, 228 | state: Binding, 229 | restrictedPages: [String]? = nil, 230 | htmlInState: Bool = false, 231 | schemeHandlers: [String: (URL) -> Void] = [:]) { 232 | self.config = config 233 | _action = action 234 | _state = state 235 | self.restrictedPages = restrictedPages 236 | self.htmlInState = htmlInState 237 | self.schemeHandlers = schemeHandlers 238 | } 239 | 240 | public func makeCoordinator() -> WebViewCoordinator { 241 | WebViewCoordinator(webView: self) 242 | } 243 | 244 | public func makeUIView(context: Context) -> WKWebView { 245 | let preferences = WKPreferences() 246 | preferences.javaScriptEnabled = config.javaScriptEnabled 247 | 248 | let configuration = WKWebViewConfiguration() 249 | configuration.allowsInlineMediaPlayback = config.allowsInlineMediaPlayback 250 | configuration.mediaTypesRequiringUserActionForPlayback = config.mediaTypesRequiringUserActionForPlayback 251 | configuration.preferences = preferences 252 | 253 | let webView = WKWebView(frame: CGRect.zero, configuration: configuration) 254 | webView.navigationDelegate = context.coordinator 255 | webView.uiDelegate = context.coordinator 256 | webView.allowsBackForwardNavigationGestures = config.allowsBackForwardNavigationGestures 257 | webView.scrollView.isScrollEnabled = config.isScrollEnabled 258 | webView.isOpaque = config.isOpaque 259 | if #available(iOS 14.0, *) { 260 | webView.backgroundColor = UIColor(config.backgroundColor) 261 | } else { 262 | webView.backgroundColor = .clear 263 | } 264 | 265 | return webView 266 | } 267 | 268 | public func updateUIView(_ uiView: WKWebView, context: Context) { 269 | if action == .idle || context.coordinator.actionInProgress { 270 | return 271 | } 272 | context.coordinator.actionInProgress = true 273 | switch action { 274 | case .idle: 275 | break 276 | case .load(let request): 277 | uiView.load(request) 278 | case .loadHTML(let pageHTML): 279 | uiView.loadHTMLString(pageHTML, baseURL: nil) 280 | case .reload: 281 | uiView.reload() 282 | case .goBack: 283 | uiView.goBack() 284 | case .goForward: 285 | uiView.goForward() 286 | case .evaluateJS(let command, let callback): 287 | uiView.evaluateJavaScript(command) { result, error in 288 | if let error = error { 289 | callback(.failure(error)) 290 | } else { 291 | callback(.success(result)) 292 | } 293 | } 294 | } 295 | } 296 | } 297 | #endif 298 | 299 | #if os(macOS) 300 | public struct WebView: NSViewRepresentable { 301 | let config: WebViewConfig 302 | @Binding var action: WebViewAction 303 | @Binding var state: WebViewState 304 | let restrictedPages: [String]? 305 | let htmlInState: Bool 306 | let schemeHandlers: [String: (URL) -> Void] 307 | 308 | public init(config: WebViewConfig = .default, 309 | action: Binding, 310 | state: Binding, 311 | restrictedPages: [String]? = nil, 312 | htmlInState: Bool = false, 313 | schemeHandlers: [String: (URL) -> Void] = [:]) { 314 | self.config = config 315 | _action = action 316 | _state = state 317 | self.restrictedPages = restrictedPages 318 | self.htmlInState = htmlInState 319 | self.schemeHandlers = schemeHandlers 320 | } 321 | 322 | public func makeCoordinator() -> WebViewCoordinator { 323 | WebViewCoordinator(webView: self) 324 | } 325 | 326 | public func makeNSView(context: Context) -> WKWebView { 327 | let preferences = WKPreferences() 328 | preferences.javaScriptEnabled = config.javaScriptEnabled 329 | 330 | let configuration = WKWebViewConfiguration() 331 | configuration.preferences = preferences 332 | 333 | let webView = WKWebView(frame: CGRect.zero, configuration: configuration) 334 | webView.navigationDelegate = context.coordinator 335 | webView.uiDelegate = context.coordinator 336 | webView.allowsBackForwardNavigationGestures = config.allowsBackForwardNavigationGestures 337 | 338 | return webView 339 | } 340 | 341 | public func updateNSView(_ uiView: WKWebView, context: Context) { 342 | if action == .idle { 343 | return 344 | } 345 | switch action { 346 | case .idle: 347 | break 348 | case .load(let request): 349 | uiView.load(request) 350 | case .loadHTML(let html): 351 | uiView.loadHTMLString(html, baseURL: nil) 352 | case .reload: 353 | uiView.reload() 354 | case .goBack: 355 | uiView.goBack() 356 | case .goForward: 357 | uiView.goForward() 358 | case .evaluateJS(let command, let callback): 359 | uiView.evaluateJavaScript(command) { result, error in 360 | if let error = error { 361 | callback(.failure(error)) 362 | } else { 363 | callback(.success(result)) 364 | } 365 | } 366 | } 367 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 368 | action = .idle 369 | } 370 | } 371 | } 372 | #endif 373 | 374 | struct WebViewTest: View { 375 | @State private var action = WebViewAction.idle 376 | @State private var state = WebViewState.empty 377 | @State private var address = "https://www.google.com" 378 | 379 | var body: some View { 380 | VStack { 381 | titleView 382 | navigationToolbar 383 | errorView 384 | Divider() 385 | WebView(action: $action, 386 | state: $state, 387 | restrictedPages: ["apple.com"], 388 | htmlInState: true) 389 | Text(state.pageHTML ?? "") 390 | .lineLimit(nil) 391 | Spacer() 392 | } 393 | } 394 | 395 | private var titleView: some View { 396 | Text(String(format: "%@ - %@", state.pageTitle ?? "Load a page", state.pageURL ?? "No URL")) 397 | .font(.system(size: 24)) 398 | } 399 | 400 | private var navigationToolbar: some View { 401 | HStack(spacing: 10) { 402 | Button("Test HTML") { 403 | action = .loadHTML(""" 404 | 405 | Hello World!
406 | Go to google 407 | 408 | """) 409 | } 410 | TextField("Address", text: $address) 411 | if state.isLoading { 412 | if #available(iOS 14, macOS 11, *) { 413 | ProgressView() 414 | .progressViewStyle(CircularProgressViewStyle()) 415 | } else { 416 | Text("Loading") 417 | } 418 | } 419 | Spacer() 420 | Button("Go") { 421 | if let url = URL(string: address) { 422 | action = .load(URLRequest(url: url)) 423 | } 424 | } 425 | Button(action: { 426 | action = .reload 427 | }) { 428 | if #available(iOS 14, macOS 11, *) { 429 | Image(systemName: "arrow.counterclockwise") 430 | .imageScale(.large) 431 | } else { 432 | Text("Reload") 433 | } 434 | } 435 | if state.canGoBack { 436 | Button(action: { 437 | action = .goBack 438 | }) { 439 | if #available(iOS 14, macOS 11, *) { 440 | Image(systemName: "chevron.left") 441 | .imageScale(.large) 442 | } else { 443 | Text("<") 444 | } 445 | } 446 | } 447 | if state.canGoForward { 448 | Button(action: { 449 | action = .goForward 450 | }) { 451 | if #available(iOS 14, macOS 11, *) { 452 | Image(systemName: "chevron.right") 453 | .imageScale(.large) 454 | } else { 455 | Text(">") 456 | } 457 | } 458 | } 459 | }.padding() 460 | } 461 | 462 | private var errorView: some View { 463 | Group { 464 | if let error = state.error { 465 | Text(error.localizedDescription) 466 | .foregroundColor(.red) 467 | } 468 | } 469 | } 470 | } 471 | 472 | struct WebView_Previews: PreviewProvider { 473 | static var previews: some View { 474 | WebViewTest() 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /SwiftUIWebView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'SwiftUIWebView' 3 | s.version = '1.0.8' 4 | s.summary = 'Fully functional, SwiftUI-ready WebView for iOS 13+ and MacOS 10.15+.' 5 | s.homepage = 'https://github.com/globulus/swiftui-webview' 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.author = { 'Gordan Glavaš' => 'gordan.glavas@gmail.com' } 8 | s.source = { :git => 'https://github.com/globulus/swiftui-webview.git', :tag => s.version.to_s } 9 | s.ios.deployment_target = '13.0' 10 | s.osx.deployment_target = '10.15' 11 | s.swift_version = '4.0' 12 | s.source_files = 'Sources/SwiftUIWebView/**/*' 13 | end 14 | --------------------------------------------------------------------------------