| /* |
| * Copyright (C) 2020 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| import SwiftUI |
| import _WebKit_SwiftUI |
| |
| struct BrowserTab : View { |
| @StateObject private var state = WebViewState(initialURL: URL(string: "https://webkit.org/")!) |
| |
| var body: some View { |
| let content = WebView(state: state) |
| .webViewNavigationPolicy(onAction: decidePolicy(for:state:)) |
| .alert(item: $externalNavigation, content: makeExternalNavigationAlert(_:)) |
| |
| #if os(macOS) |
| return content |
| .edgesIgnoringSafeArea(.all) |
| .navigationTitle(state.title.isEmpty ? "MiniBrowserSwiftUI" : state.title) |
| .toolbar { |
| ToolbarItemGroup(placement: .navigation) { |
| backItem |
| forwardItem |
| } |
| ToolbarItem(placement: .principal) { |
| urlField |
| } |
| } |
| #else |
| return content |
| // FIXME: This should be `.all`, but on iOS safe area insets do not seem to be respected |
| // correctly when embedded in a `NavigationView`. |
| .edgesIgnoringSafeArea(.bottom) |
| .navigationBarTitleDisplayMode(.inline) |
| .toolbar { |
| ToolbarItemGroup(placement: .navigationBarLeading) { |
| backItem.labelStyle(IconOnlyLabelStyle()) |
| forwardItem.labelStyle(IconOnlyLabelStyle()) |
| } |
| ToolbarItem(placement: .principal) { |
| urlField |
| } |
| } |
| #endif |
| } |
| |
| private var urlField: some View { |
| URLField( |
| url: state.url, |
| isSecure: state.hasOnlySecureContent, |
| loadingProgress: state.estimatedProgress, |
| onNavigate: onNavigate(to:) |
| ) { |
| if state.isLoading { |
| Button(action: state.stopLoading) { |
| Label("Stop Loading", systemImage: "xmark") |
| } |
| } else { |
| Button(action: state.reload) { |
| Label("Reload", systemImage: "arrow.clockwise") |
| } |
| .disabled(state.url == nil) |
| } |
| } |
| } |
| |
| private var backItem: some View { |
| Button(action: state.goBack) { |
| Label("Back", systemImage: "chevron.left") |
| .frame(minWidth: 20) |
| } |
| .disabled(!state.canGoBack) |
| } |
| |
| private var forwardItem: some View { |
| Button(action: state.goForward) { |
| Label("Forward", systemImage: "chevron.right") |
| .frame(minWidth: 20) |
| } |
| .disabled(!state.canGoForward) |
| } |
| |
| private func onNavigate(to string: String) { |
| switch UserInput(string: string) { |
| case .search(let term): |
| state.load(URL(searchTerm: term)) |
| case .url(let url): |
| state.load(url) |
| case .invalid: |
| break |
| } |
| } |
| |
| @State private var externalNavigation: ExternalURLNavigation? |
| @Environment(\.openURL) private var openURL |
| |
| private func decidePolicy(for action: NavigationAction, state: WebViewState) { |
| if let externalURL = action.request.url, !WebView.canHandle(externalURL) { |
| externalNavigation = ExternalURLNavigation( |
| source: state.url ?? .aboutBlank, destination: externalURL) |
| action.decidePolicy(.cancel) |
| } else { |
| action.decidePolicy(.allow) |
| } |
| } |
| |
| private func makeExternalNavigationAlert(_ navigation: ExternalURLNavigation) -> Alert { |
| Alert(title: Text("Allow “\(navigation.source.highLevelDomain)” to open “\(navigation.destination.scheme ?? "")”?"), |
| primaryButton: .default(Text("Allow"), action: { openURL(navigation.destination) }), |
| secondaryButton: .cancel()) |
| } |
| |
| } |
| |
| private enum UserInput { |
| case search(String) |
| case url(URL) |
| case invalid |
| |
| init(string _string: String) { |
| let string = _string.trimmingCharacters(in: .whitespaces) |
| |
| if string.isEmpty { |
| self = .invalid |
| } else if !string.contains(where: \.isWhitespace), string.contains("."), let url = URL(string: string) { |
| self = .url(url) |
| } else { |
| self = .search(string) |
| } |
| } |
| } |
| |
| private struct ExternalURLNavigation : Identifiable, Hashable { |
| var source: URL |
| var destination: URL |
| |
| var id: Self { self } |
| } |
| |
| struct BrowserTab_Previews: PreviewProvider { |
| static var previews: some View { |
| #if os(iOS) |
| NavigationView { |
| BrowserTab() |
| } |
| .navigationViewStyle(StackNavigationViewStyle()) |
| #endif |
| } |
| } |