├── .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 | 
6 |
7 | 
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 |
--------------------------------------------------------------------------------