How to search a custom text from textfield in webkit swiftUI - swift

I am building a small browser in swift ui i have build a struct to represent the wkwebview and i want to enter any text in a textfield and search on the internet using wkwebview and a google query i tried to reinstantiate the Webview(web: nil, req: URLRequest(url: URL(string: searchText)!)) but its not working for me? how can i achieve this i am pretty new to SwiftUI.Please help?
struct Webview : UIViewRepresentable {
let request: URLRequest
var webview: WKWebView?
init(web: WKWebView?, req: URLRequest) {
self.webview = WKWebView()
self.request = req
}
func makeUIView(context: Context) -> WKWebView {
return webview!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
func goBack(){
webview?.goBack()
}
func goForward(){
webview?.goForward()
}
func refresh(){
webview?.reload()
}
}
import SwiftUI
struct ContentView: View {
var webview = Webview(web: nil, req: URLRequest(url: URL(string: "https://www.apple.com")!))
#State private var searchText = ""
#State private var txt = ""
var body: some View {
ZStack {
HStack {
TextField("Search", text: $searchText,onCommit: {
print(searchText)
}
)
.keyboardType(.URL)
.frame(width: UIScreen.main.bounds.size.width * 0.75 )
}
}
webview
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button(action: {
webview.goBack()
}) {
Image(systemName: "arrow.left")
}
Spacer()
Button(action: {
webview.goForward()
}) {
Image(systemName: "arrow.right")
}
Spacer()
Button(action: {
webview.refresh()
}) {
Image(systemName: "arrow.clockwise")
}
}
}
}
}

The way that you have things set up right now (attempting to store a reference to a UIViewRepresentable) is going to lead to problems later on (like this, in fact).
I suggest you set up an interim object of some sort that can be used to store data and communicate imperatively to the WKWebView while still retaining the ability to lay things out declaratively. In my example, WebVewManager is that interim object that both the parent view and the UIViewRepresentable have access to:
class WebViewManager : ObservableObject {
var webview: WKWebView = WKWebView()
init() {
webview.load(URLRequest(url: URL(string: "https://apple.com")!))
}
func searchFor(searchText: String) {
if let searchTextNormalized = searchText.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
let url = URL(string: "https://google.com/search?q=\(searchTextNormalized)") { self.loadRequest(request: URLRequest(url: url))
}
}
func loadRequest(request: URLRequest) {
webview.load(request)
}
func goBack(){
webview.goBack()
}
func goForward(){
webview.goForward()
}
func refresh(){
webview.reload()
}
}
struct Webview : UIViewRepresentable {
var manager : WebViewManager
init(manager: WebViewManager) {
self.manager = manager
}
func makeUIView(context: Context) -> WKWebView {
return manager.webview
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
struct ContentView: View {
#StateObject private var manager = WebViewManager()
#State private var searchText = ""
#State private var txt = ""
var body: some View {
ZStack {
HStack {
TextField("Search", text: $searchText,onCommit: {
print(searchText)
manager.searchFor(searchText: searchText)
})
.keyboardType(.URL)
.frame(width: UIScreen.main.bounds.size.width * 0.75 )
}
}
Webview(manager: manager)
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button(action: {
manager.goBack()
}) {
Image(systemName: "arrow.left")
}
Spacer()
Button(action: {
manager.goForward()
}) {
Image(systemName: "arrow.right")
}
Spacer()
Button(action: {
manager.refresh()
}) {
Image(systemName: "arrow.clockwise")
}
}
}
}
}
I'm doing some simple percent encoding on the search string -- you may need to do more testing to make sure that google always accepts the search query, but this looks like it's working to me.

Related

How to resolve "AttributeGraph: cycle" warnings in the following code?

For context, the goal of the code below is to intercept a particular type of link inside a webview and handle navigation across a tabview natively (to a separate webview displaying the desired page) rather let the webview navigate itself. However, when I attempt to change the currentSelection to the desired index, I get a long list of "===AttributeGraph: cycle...===" messages. Below is the entirety of the code needed to repro this behavior:
import SwiftUI
import WebKit
#main
struct AttributeGraphCycleProofApp: App {
var body: some Scene {
WindowGroup {
ContentView(theController: Controller())
}
}
}
struct ContentView: View {
#StateObject var theController: Controller
#State var currentSelection = 0
private let selectedBackgroundColor = Color.green
var body: some View {
VStack(spacing: 12.0) {
HStack(spacing: .zero) {
ForEach(Array(0..<theController.viewModel.menuEntries.count), id: \.self) { i in
let currentMenuEntry = theController.viewModel.menuEntries[i]
Text(currentMenuEntry.title)
.padding()
.background(i == currentSelection ? selectedBackgroundColor : .black)
.foregroundColor(i == currentSelection ? .black : .gray)
}
}
TabView(selection: $currentSelection) {
let menuEntries = theController.viewModel.menuEntries
ForEach(Array(0..<menuEntries.count), id: \.self) { i in
let currentMenuEntry = theController.viewModel.menuEntries[i]
WrappedWebView(slug: currentMenuEntry.slug, url: currentMenuEntry.url) { destinationIndex in
// cycle warnings are logged when this line is executed
currentSelection = destinationIndex
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.padding()
.background(.black)
.onAppear { theController.start() }
}
}
class Controller: ObservableObject {
#Published var viewModel: ViewModel = ViewModel(menuEntries: [])
func start() {
// Represents network request to create dynamic menu entries
DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: DispatchTimeInterval.seconds(1)), execute: { [weak self] in
self?.viewModel = ViewModel.create()
})
}
}
struct ViewModel {
let menuEntries: [MenuEntry]
static func create() -> Self {
return Self(menuEntries: [
MenuEntry(title: "Domain", slug: "domain", url: "https://www.example.com/"),
MenuEntry(title: "Iana", slug: "iana", url: "https://www.iana.org/domains/reserved"),
])
}
}
struct MenuEntry {
let title: String
let slug: String
let url: String
}
struct WrappedWebView: UIViewRepresentable {
var slug: String
var url: String
var navigateToSlug: ((Int) -> Void)? = nil
func makeCoordinator() -> WrappedWebView.Coordinator { Coordinator(self) }
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
webView.isOpaque = false
if let wrappedUrl = URL(string: url), webView.url != wrappedUrl {
let request = URLRequest(url: wrappedUrl)
webView.load(request)
}
}
class Coordinator: NSObject, WKNavigationDelegate {
let parent: WrappedWebView
init(_ parent: WrappedWebView) {
self.parent = parent
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
let url = navigationAction.request.url
if url?.absoluteString == parent.url {
return .allow
}
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.parent.navigateToSlug?(1)
}
return .cancel
}
}
}
All instrumentation and memory graph debugging are failing me as they don't describe the moment the leak occurs, all I know is that the critical line causing the leaks is the assignment of navigationIndex to currentSelection.

Presenting two sheets in SwiftUI

I'm trying to present two sheets in SwiftUI. The first sheet (SecondScreen) opens up on the Main Page (tapping the Navigation Tool Bar Icon) and the second sheet is a ShareSheet which should pop up inside the SecondScreen as an option. I have used a Form to build the SecondScreen. In the Simulator and on my device, the ShareSheet doesn't appear. I hope this is just a bug and not something Apple doesn't allow without big UI changes.
I tried to open the ShareSheet, while having the SecondScreen as a .fullScreenCover., instead of .sheet but the button still doesn't react.
Example
import SwiftUI
struct ContentView: View {
#State var showMore: Bool = false
var body: some View {
NavigationView {
Text("Main Page")
.padding()
.navigationBarTitle("Main Page")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showMore.toggle()
}, label: {
Image(systemName: "ellipsis.circle")
})
.sheet(isPresented: $showMore, content: {
SecondScreen()
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct SecondScreen: View {
var body: some View {
NavigationView {
Form {
Section {
Button(action: {
ShareID (Info: "https://www.google.com")}, label: { Text("Share")
})
}
}
}
}
}
}
func ShareID(Info: String){
let infoU = Info
let av = UIActivityViewController(activityItems: [infoU], applicationActivities: nil)
UIApplication.shared.windows.first?
.rootViewController?.present(av, animated: true,
completion: nil)
}
Thank you!
this is another approach to popup your sheets, even works on my mac:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var showMore: Bool = false
var body: some View {
NavigationView {
Text("Main Page")
.padding()
.navigationBarTitle("Main Page")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showMore.toggle() }) {
Image(systemName: "ellipsis.circle")
}
.sheet(isPresented: $showMore) {
SecondScreen()
}
}
}
}
}
}
struct SecondScreen: View {
#State var shareIt = false
#State var info = "https://www.google.com"
var body: some View {
Button(action: {shareIt = true}) {
Text("Share")
}
.sheet(isPresented: $shareIt, onDismiss: {shareIt = false}) {
ShareSheet(activityItems: [info as Any])
}
}
}
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
let activityItems: [Any]
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
let callback: Callback? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { }
}

How do I display a MathJax String in a SwiftUI ScrollView?

I receive a MathJax String from an API response which needs to be shown in a WKWebView in SwiftUI. Here's a sample String
"<img alt=\"\" height=\"2305\" src=\"https:\/\/s3-us-west-2.amazonaws.com\/infinitestudent-images\/ckeditor_assets\/pictures\/37732\/content_types_of_combustion.jpg\" width=\"800\" \/>"
Here's the code that I've tried:
import SwiftUI
import WebKit
struct Model: Codable {
var mathjaxBody: String?
var answer: Answer
}
struct MathJaxView: View {
let model: Model
var body: some View {
ScrollView {
AnswerSectionView(answer: model.answer)
HTMLView(htmlString: model.mathjaxBody)
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity)
}
}
}
struct HTMLView: UIViewRepresentable {
var htmlString: String?
func makeUIView(context: Context) -> WKWebView {
WKWebView(frame: .zero)
}
func updateUIView(_ uiView: WKWebView, context: Context) {
guard let string = htmlString else { return }
uiView.loadHTMLString(string, baseURL: nil)
}
}
It need to be in a ScrollView with a few different details on top of it, which is displayed properly in the ScrollView. But, the HTMLView doesn't show up. I tried adding .frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity) But still doesn't show up.
The solution was to simply calculate the required height from evaluateJavaScript("document.body.scrollHeight") method and apply it to the WebView component with a few other methods.
struct WebView: UIViewRepresentable {
var htmlString: String?
#EnvironmentObject var viewModel: WebViewModel
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView(frame: .zero)
webView.backgroundColor = .clear
webView.navigationDelegate = context.coordinator
webView.scrollView.isScrollEnabled = false
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
guard let string = htmlString else { return }
uiView.loadHTMLString(string, baseURL: nil)
}
class Coordinator: NSObject, WKNavigationDelegate {
private var viewModel: WebViewModel
init(viewModel: WebViewModel) {
self.viewModel = viewModel
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState") { (complete, error) in
if complete != nil {
webView.evaluateJavaScript("document.body.scrollHeight") { (height, error) in
if let height = height as? CGFloat, self.viewModel.contentHeight == 0 {
self.viewModel.contentHeight = height * 0.4
}
}
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(viewModel: viewModel)
}
}
class WebViewModel: ObservableObject {
#Published var contentHeight: CGFloat = 0
}
struct MathJaxView: View {
let model: Model
var body: some View {
ScrollView {
AnswerSectionView(answer: model.answer)
WebView(htmlString: model.mathjaxBody)
.environmentObject(webViewModel)
.frame(height: webViewModel.contentHeight)
.frame(maxWidth: .infinity)
.fixedSize(horizontal: false, vertical: true)
}
}
}

LazyVGrid, List, LazyStacks don't release views from memory?

I'm playing around with the new photo picker in SwiftUI 2 and I made a simple app to show the imported images in a LazyVGrid but when scrolling down, if I imported around 150 images the app finish all the memory and it crashes (Terminated due to memory issue).
I tried the same with a LazyVStack and List but they have the same problem, I was expecting lazy items to release all the cells that goes off screen from the memory but it doesn't look like it's working.
Is this a bug or am I doing something wrong?
Here's my code:
import SwiftUI
struct Media: Identifiable {
var id = UUID()
var image: Image
}
struct ContentView: View {
#State var itemProviders: [NSItemProvider] = []
#State private var showingPhotoPicker: Bool = false
let columns = [
GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 8)
]
#State var medias: [Media] = []
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(medias) { media in
media.image
.resizable()
.scaledToFill()
.frame(width: 100, height: 100, alignment: .center)
.clipped()
}
}
}
.navigationBarTitle("Images \(medias.count)")
.navigationBarItems(leading: Button(action: {
loadImages()
}, label: {
Text("Import \(itemProviders.count) images")
}), trailing: Button(action: {
showingPhotoPicker.toggle()
}, label: {
Image(systemName: "photo.on.rectangle.angled")
}))
.sheet(isPresented: $showingPhotoPicker) {
MultiPHPickerView(itemProviders: $itemProviders)
}
}
}
func loadImages() {
for item in itemProviders {
if item.canLoadObject(ofClass: UIImage.self) {
item.loadObject(ofClass: UIImage.self) { image, error in
DispatchQueue.main.async {
guard let image = image as? UIImage else {
return
}
medias.append(Media(image: Image(uiImage: image)))
}
}
}
}
}
}
And the PhotoPickerView:
import SwiftUI
import PhotosUI
struct MultiPHPickerView: UIViewControllerRepresentable {
#Environment(\.presentationMode) private var presentationMode
#Binding var itemProviders: [NSItemProvider]
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.filter = .images
configuration.selectionLimit = 0
let controller = PHPickerViewController(configuration: configuration)
controller.delegate = context.coordinator
return controller
}
func updateUIViewController( _ uiViewController: PHPickerViewController, context: Context) {}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
#Environment(\.presentationMode) private var presentationMode
var parent: MultiPHPickerView
init( _ parent: MultiPHPickerView ) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss( animated: true )
self.parent.itemProviders = results.map(\.itemProvider)
}
}
}

Dismiss WKWebView in SwiftUI

I'm new to Swift and building an application using SwiftUI.
I'm displaying a WKWebView using code that's similar to what I've written below.
struct SomeView: View {
#State var showWebView = false
var body: some View {
ZStack {
Button(action: {
self.showWebView.toggle()
}) {
Text("Go To WebView")
.padding()
.foregroundColor(.black)
.font(.title)
}
.sheet(isPresented: $showWebView, content: {
WebView(url: "https://www.apple.com/")
})
}
}
}
struct WebView: UIViewRepresentable {
var url: String
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
return webView
}
func updateUIView(_ uiView: WebView.UIViewType, context: UIViewRepresentableContext<WebView>) {
let urlRequest = URLRequest.init(url: URL.init(string: self.url)!)
uiView.navigationDelegate = context.coordinator
uiView.load(urlRequest)
}
}
class Coordinator: NSObject, WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url?.absoluteString {
if url.contains("apple") {
let someViewModel = SomeViewModel()
someViewModel.processRequest(url: url)
}
}
decisionHandler(.allow)
}
}
The code works as expected. I'm able to display a WKWebView showing the desired website. However, I'm struggling to figure out how to dismiss the WKWebView and navigate to another page once I've invoked someViewModel.processRequest(). Could you please assist?
I would do the whole WebView thing a bit different, which should (as far as I understand your problem) solve your situation:
import SwiftUI
import SafariServices
struct SomeView: View {
#State var showWebView = false
var body: some View {
ZStack {
Button(action: {
self.showWebView.toggle()
}) {
Text("Go To WebView")
.padding()
.foregroundColor(.black)
.font(.title)
}
.sheet(isPresented: $showWebView, content: {
WebView(url: URL(string: "https://www.apple.com/")!) })
}
}
}
struct WebView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: UIViewControllerRepresentableContext<WebView>) -> SFSafariViewController {
return SFSafariViewController(url: url)
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<WebView>) {
}
}