QLPreviewController missing navigation bar in SwiftUI - swift

QLPreviewController navigation bar missing when I presented in sheet.
It looks like this :
How can I show up top navigation bar or navigationItem?
QuickLookController UIKit representable in swiftUI
.sheet(isPresented: $showQuickLook, onDismiss: {self.showQuickLook = false}) {
if self.selectedURL != nil {
QuickLookController(url: self.selectedURL!) {
self.showQuickLook = false
}.edgesIgnoringSafeArea(.all)
}
}
struct QuickLookController: UIViewControllerRepresentable {
var url: URL
var onDismiss: () -> Void
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ uiViewController: QLPreviewController, context: UIViewControllerRepresentableContext<QuickLookController>) {
uiViewController.reloadData()
}
func makeUIViewController(context: Context) -> QLPreviewController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
controller.reloadData()
return controller
}
class Coordinator: NSObject, QLPreviewControllerDataSource {
var parent: QuickLookController
init(_ qlPreviewController: QuickLookController) {
self.parent = qlPreviewController
super.init()
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return self.parent.url as QLPreviewItem
}
}
}

QuickLook preview does not have own navigation controller, it is our responsibility to provided one.
Here is possible approach. Tested with Xcode 11.4 / iOS 13.4
struct TestQLPreviewController: View {
#State private var showQuickLook = false
// just for demo - document.pdf is located in main bundle
#State private var selectedURL = Bundle.main.url(forResource: "document", withExtension: "pdf")
var body: some View {
Button("Show") { self.showQuickLook.toggle() }
.sheet(isPresented: $showQuickLook, onDismiss: {self.showQuickLook = false}) {
if self.selectedURL != nil {
QuickLookController(url: self.selectedURL!) {
self.showQuickLook = false
}
}
}
}
}
struct QuickLookController: UIViewControllerRepresentable {
var url: URL
var onDismiss: () -> Void
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ viewController: UINavigationController, context: UIViewControllerRepresentableContext<QuickLookController>) {
if let controller = viewController.topViewController as? QLPreviewController {
controller.reloadData()
}
}
func makeUIViewController(context: Context) -> UINavigationController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
controller.reloadData()
return UINavigationController(rootViewController: controller)
}
class Coordinator: NSObject, QLPreviewControllerDataSource {
var parent: QuickLookController
init(_ qlPreviewController: QuickLookController) {
self.parent = qlPreviewController
super.init()
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return self.parent.url as QLPreviewItem
}
}
}

Here is an implementation that can handle multiple urls:
import SwiftUI
import QuickLook
struct QuickLookView: UIViewControllerRepresentable {
var urls: [URL]
func updateUIViewController(_ uiViewController: QLPreviewController,
context: UIViewControllerRepresentableContext<QuickLookView>) {
uiViewController.reloadData()
}
func makeUIViewController(context: Context) -> QLPreviewController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
controller.reloadData()
return controller
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, QLPreviewControllerDataSource {
var parent: QuickLookView
init(_ qlPreviewController: QuickLookView) {
self.parent = qlPreviewController
super.init()
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return parent.urls.count
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return parent.urls[index] as QLPreviewItem
}
}
}
Then wrap the QuickLookView in a container and access that from your views:
struct QuickLookContainerView: View {
let urls: [URL]
var body: some View {
NavigationView {
QuickLookView(urls: urls)
}
}
}

Related

Set property on ViewController in UIViewControllerRepresentable

How can I change a property on a UIViewController presented via. a UIViewControllerRepresentable ?
Sample code of how I would expect it to work, however it doesn't. How can I make it work?
(color is just a example, please don't focus on that)
class MyViewController: UIViewController {
var color: UIColor? = nil {
didSet {
guard isViewLoaded else { return }
view.layer.backgroundColor = color?.cgColor
}
}
override func viewDidLoad() {
view.layer.backgroundColor = color?.cgColor
}
}
struct MyView: UIViewControllerRepresentable {
#State private var color: UIColor?
func makeUIViewController(context: UIViewControllerRepresentableContext<MyView>) -> MyViewController {
let viewController = MyViewController()
viewController.color = color // always nil?
return viewController
}
func updateUIViewController(_ uiViewController: MyViewController,
context: UIViewControllerRepresentableContext<MyView>) {
uiViewController.color = color // always nil?
}
}
extension MyView {
func color(_ color: UIColor) -> MyView {
self.color = color // does nothing?
return self
}
}
struct ContentView: View {
var body: some View {
MyView()
.color(.magenta)
}
}
Here is possible approach (if you expect that color can be modified externally, as it is seen). Tested with Xcode 11.4 / iOS 13.4
struct MyView: UIViewControllerRepresentable {
#Binding var color: UIColor?
func makeUIViewController(context: UIViewControllerRepresentableContext<MyView>) -> MyViewController {
let viewController = MyViewController()
viewController.color = color // always nil?
return viewController
}
func updateUIViewController(_ uiViewController: MyViewController,
context: UIViewControllerRepresentableContext<MyView>) {
uiViewController.color = color // always nil?
}
}
struct ContentView: View {
#State private var color: UIColor? = .magenta
var body: some View {
MyView(color: $color)
// MyView(color: .constant(.magenta)) // alternate usage
}
}
Another solution based on idea from Asperi's answer:
struct MyView: UIViewControllerRepresentable {
private class State: ObservableObject {
var color: UIColor?
}
#Binding private var state: State
init() {
_state = .constant(State())
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MyView>) -> MyViewController {
return MyViewController()
}
func updateUIViewController(_ uiViewController: MyViewController,
context: UIViewControllerRepresentableContext<MyView>) {
uiViewController.color = state.color
}
}
extension MyView {
func color(_ color: UIColor) -> MyView {
self.state.color = color
return self
}
}
Or a even simpler version, we just use #Binding for the wrapped ViewController directly
struct MyView: UIViewControllerRepresentable {
#Binding private var viewController: MyViewController
init() {
_viewController = .constant(MyViewController())
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MyView>) -> MyViewController {
return viewController
}
func updateUIViewController(_ uiViewController: MyViewController,
context: UIViewControllerRepresentableContext<MyView>) {
}
}
extension MyView {
func color(_ color: UIColor) -> MyView {
self.viewController.color = color
return self
}
}

How to change position of SearchBar in NavigationBar?

I followed this article on how to display a SearchBar in the NavigationBar. I integrated it like this into my view:
struct ExploreView: View {
#ObservedObject var searchBar = SearchBar()
var body: some View {
NavigationView {
ZStack {
Color(red: 250/255, green: 250/255, blue: 250/255)
.edgesIgnoringSafeArea(.all)
VStack(spacing: 0) {
Image(R.image.navigationBarBackground)
.resizable()
.scaledToFit()
.frame(width: UIScreen.main.bounds.width)
.edgesIgnoringSafeArea(.all)
Spacer()
}
}
.navigationBarTitle("", displayMode: .inline)
.add(self.searchBar)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
class SearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
}
}
extension SearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: SearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) {
}
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
}
It look's like this: SearchBar: inactive, SearchBar: active
But I would like to have the inactive SearchBar in the same position as the active SearchBar to avoid the free space. In the end it should look like the SearchBar in the Instagram App. Does anyone know how to do this?
Add this piece of code inside your init() method of SearchBar. It will make search bar at same position when it's active.
self.searchController.hidesNavigationBarDuringPresentation = false
If you want to set search bar to navigation bar title instead of text, inside your overlay(_:)
Change this code
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
}
To
ViewControllerResolver { viewController in
viewController.navigationItem.titleView = self.searchBar.searchController.searchBar
}

The function does not work in UIViewControllerRepresentable

I am trying to implement the unsplash-photopicker-ios component in SwiftUI through the UIViewControllerRepresentable. The view starts successfully, but the unsplashPhotoPicker function does not work, it always returns nil, can someone tell me what the problem is.
import SwiftUI
import UnsplashPhotoPicker
struct UnsplashImagePicker: UIViewControllerRepresentable {
var urlUnsplashImage = [UnsplashPhoto]()
let configuration = UnsplashPhotoPickerConfiguration(
accessKey: "f99d21d6eb682196455dd29b621688aff2d525c7c3a7f95bfcb05d497f38f5dc",
secretKey: "ccff858162e795c062ce13e9d16a2cf607076d0eb185141e35b14f589b1cd713",
allowsMultipleSelection: false)
func makeUIViewController(context: UIViewControllerRepresentableContext<UnsplashImagePicker>) -> UnsplashPhotoPicker {
let unsplashImagePicker = UnsplashPhotoPicker(configuration: configuration)
unsplashImagePicker.delegate = context.coordinator
return unsplashImagePicker
}
func makeCoordinator() -> UnsplashImagePicker.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UnsplashPhotoPickerDelegate, UINavigationControllerDelegate {
var parent: UnsplashImagePicker
init(_ parent: UnsplashImagePicker) {
self.parent = parent
}
func unsplashPhotoPicker(_ photoPicker: UnsplashPhotoPicker, didSelectPhotos photos: [UnsplashPhoto]) {
print(photos)
}
func unsplashPhotoPickerDidCancel(_ photoPicker: UnsplashPhotoPicker) {
print("Unsplash photo picker did cancel")
}
}
func updateUIViewController(_ uiViewController: UnsplashPhotoPicker, context: UIViewControllerRepresentableContext<UnsplashImagePicker>) {
}
}
You need to present the Unsplashpicker from a different UIViewcontroller.The Unsplashpicker could not be directly used as UIViewControllerRepresentable.
import SwiftUI
import UIKit
import UnsplashPhotoPicker
struct UnsplashPresenter: UIViewControllerRepresentable {
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
//
}
func makeUIViewController(context: Context) -> UIViewController {
return UnsplashPickerVC()
}
typealias UIViewControllerType = UIViewController
}
class UnsplashPickerVC: UIViewController, UnsplashPhotoPickerDelegate {
func unsplashPhotoPicker(_ photoPicker: UnsplashPhotoPicker, didSelectPhotos photos: [UnsplashPhoto]) {
print("=============== New Photo Selected =============== \n")
print(photos)
}
func unsplashPhotoPickerDidCancel(_ photoPicker: UnsplashPhotoPicker) {
print("did cancel")
}
var outputImage = UIImage(named: "Instructions.png")
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.frame = CGRect(x: (self.view.frame.size.width / 2) - 25, y: (self.view.frame.size.height / 2) - 50, width: 100, height: 50)
button.backgroundColor = UIColor.blue
button.setTitle("Pick Images", for: .normal)
button.addTarget(self, action: #selector(pickImages), for: .touchUpInside)
self.view.addSubview(button)
}
#objc func pickImages(sender: UIBarButtonItem) {
let configuration = UnsplashPhotoPickerConfiguration(
accessKey: "<Your Access Key >",
secretKey: "<Your Secret Key >"
)
let photoPicker = UnsplashPhotoPicker(configuration: configuration)
photoPicker.photoPickerDelegate = self
present(photoPicker, animated: true, completion: nil)
}
}

How to implement WKUIDelegate into SwiftUI WKWebView?

I'm creating a web app on Xcode v11 and having a trouble implementing WKUIDelegate to display Javascript alert and confirm properly on the web app.
I got a very simple webview app with below code on ContentView.swift but not sure how to implement WKUIDelegate properly with this code.
import SwiftUI
import WebKit
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 reload(){
webview?.reload()
}
}
struct ContentView: View {
let webview = Webview(web: nil, req: URLRequest(url: URL(string: "https://google.com")!))
var body: some View {
VStack {
webview
HStack() {
Button(action: {
self.webview.goBack()
}){
Image(systemName: "chevron.left")
}.padding(32)
Button(action: {
self.webview.reload()
}){
Image(systemName: "arrow.clockwise")
}.padding(32)
Button(action: {
self.webview.goForward()
}){
Image(systemName: "chevron.right")
}.padding(32)
}.frame(height: 32)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
What is the best way to implement WKUIDelegate into this code? So, it will display Javascript Alert and Confirm properly.
You need to use Coordinator and then conform to WKUIDelegate:
class Coordinator: NSObject, WKUIDelegate {
var parent: Webview
init(_ parent: Webview) {
self.parent = parent
}
// Delegate methods go here
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: #escaping () -> Void) {
// alert functionality goes here
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
Then ensure your updateUIView(..) sets the uiDelegate to the context.coordinator:
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.uiDelegate = context.coordinator
[...]
}
If you want to conform to WKNavigationDelegate then conform to it and set the navigationDelegate to the context.coordinator as well.
Full code here:
import SwiftUI
import WebKit
struct Webview : UIViewRepresentable {
let request: URLRequest
var webview: WKWebView?
init(web: WKWebView?, req: URLRequest) {
self.webview = WKWebView()
self.request = req
}
class Coordinator: NSObject, WKUIDelegate {
var parent: Webview
init(_ parent: Webview) {
self.parent = parent
}
// Delegate methods go here
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: #escaping () -> Void) {
// alert functionality goes here
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
return webview!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.uiDelegate = context.coordinator
uiView.load(request)
}
func goBack(){
webview?.goBack()
}
func goForward(){
webview?.goForward()
}
func reload(){
webview?.reload()
}
}

SwiftUI coordinator not updating the containing view's property

So I've wrapped WKWebView in an UIViewRepresentable and built a coordinator in order to access its navigation delegate's functions. In the webView(_:didFinish:) function I am trying to update the view's didFinishLoading variable. If I print right after assigning, it prints true - the expected behavior. But, in the parent view, when I call the getHTML function, it prints false - even if I wait until the WKWebView is fully loaded.
Here is the code:
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
#Binding var link: String
init(link: Binding<String>) {
self._link = link
}
private var didFinishLoading: Bool = false
let webView = WKWebView()
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
self.webView.load(URLRequest(url: URL(string: self.link)!))
self.webView.navigationDelegate = context.coordinator
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<WebView>) {
return
}
class Coordinator: NSObject, WKNavigationDelegate {
private var webView: WebView
init(_ webView: WebView) {
self.webView = webView
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("WebView: navigation finished")
self.webView.didFinishLoading = true
}
}
func makeCoordinator() -> WebView.Coordinator {
Coordinator(self)
}
func getHTML(completionHandler: #escaping (Any?) -> ()) {
print(self.didFinishLoading)
if (self.didFinishLoading) {
self.webView.evaluateJavaScript(
"""
document.documentElement.outerHTML.toString()
"""
) { html, error in
if error != nil {
print("WebView error: \(error!)")
completionHandler(nil)
} else {
completionHandler(html)
}
}
}
}
}
struct WebView_Previews: PreviewProvider {
#State static var link = "https://apple.com"
static var previews: some View {
WebView(link: $link)
}
}
Here is your code, a bit modified for demo, with used view model instance of ObservableObject holding your loading state.
import SwiftUI
import WebKit
import Combine
class WebViewModel: ObservableObject {
#Published var link: String
#Published var didFinishLoading: Bool = false
init (link: String) {
self.link = link
}
}
struct WebView: UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
let webView = WKWebView()
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
self.webView.navigationDelegate = context.coordinator
if let url = URL(string: viewModel.link) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<WebView>) {
return
}
class Coordinator: NSObject, WKNavigationDelegate {
private var viewModel: WebViewModel
init(_ viewModel: WebViewModel) {
self.viewModel = viewModel
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("WebView: navigation finished")
self.viewModel.didFinishLoading = true
}
}
func makeCoordinator() -> WebView.Coordinator {
Coordinator(viewModel)
}
}
struct WebViewContentView: View {
#ObservedObject var model = WebViewModel(link: "https://apple.com")
var body: some View {
VStack {
TextField("", text: $model.link)
WebView(viewModel: model)
if model.didFinishLoading {
Text("Finished loading")
.foregroundColor(Color.red)
}
}
}
}
struct WebView_Previews: PreviewProvider {
static var previews: some View {
WebViewContentView()
}
}