Passing data from UIViewControllerRepresentable to UIViewController - swift

I'm trying to pass data from a SwiftUI struct (a first name and a last name) and can't seem to update the variables in my UIViewController with the data in my UIViewControllerRepresentable.
I've checked and confirmed that the data I'm trying to pass in from my SwiftUI view is correct. What do I need to do/change to update the firstName and lastName variables in my UIViewController?
import UIKit
import PDFKit
import SwiftUI
class PDFPreviewViewController: UIViewController {
public var documentData: Data?
var firstName: String = "firstName did not load"
var lastName: String = "lastName did not load"
#IBOutlet weak var pdfView: PDFView!
override func viewDidLoad() {
super.viewDidLoad()
let pdfCreator = PDFCreator(firstName: firstName, lastName: lastName, format: "test format")
documentData = pdfCreator.createReleasePDF()
if let data = documentData {
pdfView.document = PDFDocument(data: data)
pdfView.autoScales = true
}
}
}
struct PDFPreviewController: UIViewControllerRepresentable {
var release: ModelRelease
let vc = PDFPreviewViewController()
func makeUIViewController(context: UIViewControllerRepresentableContext<PDFPreviewController>) -> UIViewController {
let storyboard = UIStoryboard(name: "Preview", bundle: Bundle.main)
let controller = storyboard.instantiateViewController(identifier: "Preview")
vc.firstName = release.firstName
vc.lastName = release.lastName
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<PDFPreviewController>) {
}
}
struct PDFPreviewControllerWrapper: View {
#Environment(\.presentationMode) var presentationMode
var release: ModelRelease
var body: some View {
NavigationView {
PDFPreviewController(release: release)
.navigationBarTitle(Text("Preview"))
.navigationBarItems(trailing: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
.fontWeight(.bold)
}
)
}
}
}

Creating view controller representable you set parameters to one controller but return another. Probably you meant the following:
//let vc = PDFPreviewViewController() // don't think it is needed at all
func makeUIViewController(context: UIViewControllerRepresentableContext<PDFPreviewController>) -> UIViewController {
let storyboard = UIStoryboard(name: "Preview", bundle: Bundle.main)
let controller = storyboard.instantiateViewController(identifier:
"Preview") as! PDFPreviewViewController
controller.firstName = release.firstName // << here !
controller.lastName = release.lastName // << and here !
return controller
}

Related

How to hide keyboard when closing sheet swiftui

I have place picker sheet and whenever I select a place the keyboard remains stuck on the main screen, I can t hide
my place picker view:
struct PlacePicker: UIViewControllerRepresentable {
func makeCoordinator() -> GooglePlacesCoordinator {
GooglePlacesCoordinator(self)
}
#Environment(\.presentationMode) var presentationMode
#Binding var address: String
#Binding var latitude: Double
#Binding var longitude: Double
func makeUIViewController(context: UIViewControllerRepresentableContext<PlacePicker>) -> GMSAutocompleteViewController {
GMSPlacesClient.provideAPIKey("xxxxx")
let autocompleteController = GMSAutocompleteViewController()
autocompleteController.delegate = context.coordinator
let fields: GMSPlaceField = GMSPlaceField(rawValue:UInt(GMSPlaceField.name.rawValue) |
UInt(GMSPlaceField.placeID.rawValue) |
UInt(GMSPlaceField.coordinate.rawValue) |
GMSPlaceField.addressComponents.rawValue |
GMSPlaceField.formattedAddress.rawValue)
autocompleteController.placeFields = fields
let filter = GMSAutocompleteFilter()
filter.type = .address
autocompleteController.autocompleteFilter = filter
return autocompleteController
}
func updateUIViewController(_ uiViewController: GMSAutocompleteViewController, context: UIViewControllerRepresentableContext<PlacePicker>) {
}
class GooglePlacesCoordinator: NSObject, UINavigationControllerDelegate, GMSAutocompleteViewControllerDelegate {
var parent: PlacePicker
init(_ parent: PlacePicker) {
self.parent = parent
}
func viewController(_ viewController: GMSAutocompleteViewController, didAutocompleteWith place: GMSPlace) {
DispatchQueue.main.async {
print(place.description.description as Any)
self.parent.address = place.formattedAddress ?? "adresa gresita"
self.parent.presentationMode.wrappedValue.dismiss()
self.parent.latitude=place.coordinate.latitude
self.parent.longitude=place.coordinate.longitude
}
}
func viewController(_ viewController: GMSAutocompleteViewController, didFailAutocompleteWithError error: Error) {
print("Error: ", error.localizedDescription)
}
func wasCancelled(_ viewController: GMSAutocompleteViewController) {
parent.presentationMode.wrappedValue.dismiss()
}
}
The way that I call the sheet:
#State var addressString: String = ""
#State var address = ""
#State var openPlacePicker = false
var body: some View{
HStack{
TextField("\(address)", text: $addressString)
.onTapGesture {
openPlacePicker.toggle()
}
}.sheet(isPresented: $openPlacePicker) {
PlacePicker()
}
}
}
How I can hide the keyboard after the placePicker selection of the location is done?
Can I add something to the PlacePicker class? Whenever the selection is done, to dismiss the keyboard or something?

How can I update my Window title for macOS Storyboard Cocoa project via an action?

This is my project:
import Cocoa
import SwiftUI
var appName: String = "My App Name"
class ViewController: NSViewController {
override func viewWillAppear() {
let controller = NSHostingController(rootView: ContentView())
self.view = controller.view
self.view.window?.title = appName
}
}
struct ContentView: View {
var body: some View {
VStack {
Button("Change") {
appName += " updated!"
print(appName)
}
}
.frame(width: 400.0, height: 300.0)
}
}
My goal is be able to update my window title, I am be able to update the variable that holds my app name but since viewWillAppear function would not be triggered I am unable to update my window title there. I was thinking to use a notification there but in this case I am not sure if it is the right feet there, because it would need to post and receive notification, what is the approach for solving this problem?
You can introduce a ViewModel storing title as #Published and from the Button action update that property rather than a global appName. In your hosting controller, you can subscribe to changes of title, since it is #Published and update the window's title whenever the view model's title property is updated.
import Cocoa
import Combine
import SwiftUI
var appName: String = "My App Name"
final class ViewModel: ObservableObject {
#Published var title: String = appName
}
class ViewController: NSViewController {
private let viewModel = ViewModel()
private var titleObservation: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
// Observe the title of the view model and update the window's title whenever it changes
titleObservation = viewModel.$title.sink { [weak self] newTitle in
self?.view.window?.title = newTitle
}
}
override func viewWillAppear() {
let controller = NSHostingController(rootView: ContentView(viewModel: viewModel))
self.view = controller.view
self.view.window?.title = viewModel.title
}
}
struct ContentView: View {
#ObservedObject private var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack {
Button("Change") {
viewModel.title += " updated!"
}
}
.frame(width: 400.0, height: 300.0)
}
}
Alternatively, you can simply inject a closure for the Button action into your view from the hosting controller and update the window's title from the closure.
class ViewController: NSViewController {
override func viewWillAppear() {
let contentView = ContentView(
onButtonTap: { // this closure will be executed every time Button is tapped
appName += " updated!"
self.view.window?.title = appName
print(appName)
}
)
let controller = NSHostingController(rootView: contentView)
self.view = controller.view
self.view.window?.title = appName
}
}
struct ContentView: View {
// Closure to execute when `Button` is tapped
private let onButtonTap: () -> Void
init(onButtonTap: #escaping () -> Void) {
self.onButtonTap = onButtonTap
}
var body: some View {
VStack {
Button("Change", action: onButtonTap)
}
.frame(width: 400.0, height: 300.0)
}
}

Calling swiftUI View from UIKIt and pass data with or without userdefaults

I have been trying to call SwiftUI view from UIViewController but i dont know the right way to do it.
I have trying using use Userdefaults but SwiftUI view complains that URL passed via userdefaults is nil
this is the view
import SwiftUI
import QuickLook
import UIKit
struct PreviewController: UIViewControllerRepresentable {
let url: URL
var error: Binding<Bool>
func makeUIViewController(context: Context) -> QLPreviewController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
controller.isEditing = false
return controller
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
func updateUIViewController(
_ uiViewController: QLPreviewController, context: Context) {}
class Coordinator: QLPreviewControllerDataSource {
var parent: PreviewController
init(parent: PreviewController) {
self.parent = parent
}
func numberOfPreviewItems(
in controller: QLPreviewController
) -> Int {
return 1
}
func previewController(
_ controller: QLPreviewController, previewItemAt index: Int
) -> QLPreviewItem {
guard self.parent.url.startAccessingSecurityScopedResource()
else {
return NSURL(fileURLWithPath: parent.url.path)
}
defer {
self.parent.url.stopAccessingSecurityScopedResource()
}
return NSURL(fileURLWithPath: self.parent.url.path)
}
}
}
struct ProjectDocumentOpener: View {
#Binding var open: Bool
#State var errorInAccess = false
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 0) {
let url = URL(string: UserDefaults.standard.string(forKey: "documentLink")!)
PreviewController(url: url!, error: $errorInAccess)
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarTitle(
Text(URL(string: UserDefaults.standard.string(forKey: "documentLink")!)?.lastPathComponent ?? "")
)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Done") {
self.open = false
}
}
}
}
}
This is how i have been calling it in UIViewController
UserDefaults.standard.set(docURL, forKey: "documentLink")
self.navigationController?.pushViewController(UIHostingController(rootView: ProjectDocumentOpener(open: .constant(true))), animated: true)
I tried to google and find a proper resource which explains how to use a SwiftUI view with UIViewController but cant find any resource suitable for my needs.

How to pass chain view controller presenter with observable

I'm new in the RxSwift development and I've an issue while presentation a view controller.
My MainViewController is just a table view and I would like to present detail when I tap on a item of the list.
My DetailViewController is modally presented and needs a ViewModel as input parameter.
I would like to avoid to dismiss the DetailViewController, I think that the responsability of dismiss belongs to the one who presented the view controller, i.e the dismiss should happen in the MainViewController.
Here is my current code
DetailsViewController
class DetailsViewController: UIViewController {
#IBOutlet weak private var doneButton: Button!
#IBOutlet weak private var label: Label!
let viewModel: DetailsViewModel
private let bag = DisposeBag()
var onComplete: Driver<Void> {
doneButton.rx.tap.take(1).asDriver(onErrorJustReturn: ())
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
bind()
}
private func bind() {
let ouput = viewModel.bind()
ouput.id.drive(idLabel.rx.text)
.disposed(by: bag)
}
}
DetailsViewModel
class DetailsViewModel {
struct Output {
let id: Driver<String>
}
let item: Observable<Item>
init(with vehicle: Observable<Item>) {
self.item = item
}
func bind() -> Output {
let id = item
.map { $0.id }
.asDriver(onErrorJustReturn: "Unknown")
return Output(id: id)
}
}
MainViewController
class MainViewController: UIViewController {
#IBOutlet weak private var tableView: TableView!
private var bag = DisposeBag()
private let viewModel: MainViewModel
private var detailsViewController: DetailsViewController?
override func viewDidLoad(_ animated: Bool) {
super.viewDidLoad(animated)
bind()
}
private func bind() {
let input = MainViewModel.Input(
selectedItem: tableView.rx.modelSelected(Item.self).asObservable()
)
let output = viewModel.bind(input: input)
showItem(output.selectedItem)
}
private func showItem(_ item: Observable<Item>) {
let viewModel = DetailsViewModel(with: vehicle)
detailsViewController = DetailsController(with: viewModel)
item.flatMapFirst { [weak self] item -> Observable<Void> in
guard let self = self,
let detailsViewController = self.detailsViewController else {
return Observable<Void>.never()
}
self.present(detailsViewController, animated: true)
return detailsViewController.onComplete.asObservable()
}
.subscribe(onNext: { [weak self] in
self?.detailsViewController?.dismiss(animated: true)
self?.detailsViewController? = nil
})
.disposed(by: bag)
}
}
MainViewModel
class MainViewModel {
struct Input {
let selectedItem: Observable<Item>
}
struct Output {
let selectedItem: Observable<Item>
}
func bind(input: Input) -> Output {
let selectedItem = input.selectedItem
.throttle(.milliseconds(500),
latest: false,
scheduler: MainScheduler.instance)
.asObservable()
return Output(selectedItem: selectedItem)
}
}
My issue is on showItem of MainViewController.
I still to think that having the DetailsViewController input as an Observable isn't working but from what I understand from Rx, we should use Observable as much as possible.
Having Item instead of Observable<Item> as input could let me use this kind of code:
item.flatMapFirst { item -> Observable<Void> in
guard let self = self else {
return Observable<Void>.never()
}
let viewModel = DetailsViewModel(with: item)
self.detailsViewController = DetailsViewController(with: viewModel)
guard let detailsViewController = self.detailsViewController else {
return Observable<Void>.never()
}
present(detailsViewController, animated: true)
return detailsViewController
}
.subscribe(onNext: { [weak self] in
self?.detailsViewController?.dismiss(animated: true)
self?.detailsViewController = nil
})
.disposed(by: bag)
What is the right way to do this?
Thanks
You should not "use Observable as much as possible." If an object is only going to ever have to deal with a single item, then just pass the item. For example if a label is only ever going to display "Hello World" then just assign the string to the label's text property. Don't bother wrapping it in a just and binding it to the label's rx.text.
Your second option is much closer to what you should have. It's a fine idea.
You might find my CLE library interesting. It takes care of the issue you are trying to handle here.

QLPreviewController showing file then going blank in SwiftUI

I've added a UIViewControllerRepresentable for UIKit's QLPreviewController which I've found in a related question:
struct QuickLookView: UIViewControllerRepresentable {
var url: URL
var onDismiss: (() -> Void) = { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ viewController: UINavigationController, context: UIViewControllerRepresentableContext<Self>) {
(viewController.topViewController as? QLPreviewController)?.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: QuickLookView
init(_ qlPreviewController: QuickLookView) {
self.parent = qlPreviewController
super.init()
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
self.parent.url as QLPreviewItem
}
}
}
In my app, I download a file (jpg/png/pdf) via Alamofire:
let destination: DownloadRequest.Destination = { _, _ in
let documentsURL = FileManager.default.documentsDirectory
.appendingPathComponent(document.id.string)
.appendingPathComponent(document.name ?? "file.jpg")
return (documentsURL, [.removePreviousFile, .createIntermediateDirectories])
}
AF
.download(url, to: destination)
.responseURL { (response) in
guard let url = response.fileURL else { return }
self.fileURL = url
self.isShowingDoc = true
}
...and pass its local url to the QuickLookView to present it:
#State private var isShowingDoc = false
#State private var fileURL: URL?
var body: some View {
// ...
.sheet(isPresented: $isShowingDoc, onDismiss: { isShowingDoc = false }) {
QuickLookView(url: fileURL!) {
isShowingDoc = false
}
}
}
What happens is that the QuickLookView opens as sheet, the file flashes (is displayed for like 0.1 seconds) and then the view goes blank:
I checked the Documents folder of the app in Finder and the file is there and matches the url passed to the QuickLookView. I've noticed that when the view is open, and I then delete the file from the folder via Finder, then the view will throw an error saying there's no such file – that means it did read it properly before it was deleted.
Note: I read somewhere that the QL controller has had issues when placed inside a navigation controller. In my view hierarchy, my views are embedded inside a NavigationView – might that cause issues?
How do I solve this?
You just need to update the view before presenting the sheet otherwise it wont work. It can be the button title, opacity or anything. Although it looks like a hack it works fine. I will be very glad if someone explains why it happens and if there is a proper way to make it work without updating the view.
import SwiftUI
struct ContentView: View {
#State private var fileURL: URL!
#State private var isDisabled = false
#State private var isDownloadFinished = false
#State private var buttonTitle: String = "Download PDF"
private let url = URL(string: "https://www.dropbox.com/s/bxrhk6194lf0n73/macpro_mid2010-macpro_mid2012.pdf?dl=1")!
var body: some View {
Button(buttonTitle) {
isDisabled = true
buttonTitle = "Downloading..."
URLSession.shared.downloadTask(with: url) { location, response, error in
guard
let location = location, error == nil,
let suggestedFilename = (response as? HTTPURLResponse)?.suggestedFilename,
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
else { return }
fileURL = documentDirectory.appendingPathComponent(suggestedFilename)
if !FileManager.default.fileExists(atPath: fileURL.path) {
do {
try FileManager.default.moveItem(at: location, to: fileURL)
} catch {
print(error)
}
}
DispatchQueue.main.async {
isDownloadFinished = true
buttonTitle = "" // you need to change the view prefore presenting the sheet otherwise it wont work
}
}.resume()
}
.disabled(isDisabled == true)
.sheet(isPresented: $isDownloadFinished) {
isDisabled = false
isDownloadFinished = false
fileURL = nil
buttonTitle = "Download PDF"
} content: {
if isDownloadFinished {
PreviewController(previewItems: [PreviewItem(url: fileURL, title: fileURL?.lastPathComponent)], index: 0)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
import QuickLook
struct PreviewController: UIViewControllerRepresentable {
var previewItems: [PreviewItem] = []
var index: Int
func makeCoordinator() -> Coordinator { .init(self) }
func updateUIViewController(_ viewController: UINavigationController, context: UIViewControllerRepresentableContext<Self>) {
(viewController.topViewController as? QLPreviewController)?.reloadData()
}
func makeUIViewController(context: Context) -> UINavigationController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
controller.delegate = context.coordinator
controller.reloadData()
return .init(rootViewController: controller)
}
class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
let previewController: PreviewController
init(_ previewController: PreviewController) {
self.previewController = previewController
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
previewController.previewItems.count
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
previewController.previewItems[index]
}
}
}
class PreviewItem: NSObject, QLPreviewItem {
var previewItemURL: URL?
var previewItemTitle: String?
init(url: URL? = nil, title: String? = nil) {
previewItemURL = url
previewItemTitle = title
}
}
I finally got it to work – big thanks to Leo Dabus for his help in the comments.
Here's my currently working code:
#State private var isShowingDoc = false
#State private var isLoadingFile = false
#State private var fileURL: URL?
var body: some View {
Button {
let destination: DownloadRequest.Destination = { _, _ in
let documentsURL = FileManager.default.documentsDirectory
.appendingPathComponent(document.id.string)
.appendingPathComponent(document.name ?? "file.jpg")
return (documentsURL, [.removePreviousFile, .createIntermediateDirectories])
}
isLoadingFile = true
AF
.download(url, to: destination)
.responseURL { (response) in
self.isLoadingFile = false
guard let url = response.fileURL else { return }
isShowingDoc = true
self.fileURL = url
}
} label: {
VStack {
Text("download")
if isLoadingFile {
ActivityIndicator(style: .medium)
}
}
}
.sheet(isPresented: $isShowingDoc, onDismiss: { isShowingDoc = false }) {
QuickLookView(url: fileURL!)
}
}
with this QuickLookView: (mostly unchanged)
struct QuickLookView: UIViewControllerRepresentable {
var url: URL
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ viewController: UINavigationController, context: UIViewControllerRepresentableContext<Self>) {
(viewController.topViewController as? QLPreviewController)?.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: QuickLookView
init(_ qlPreviewController: QuickLookView) {
self.parent = qlPreviewController
super.init()
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
self.parent.url as QLPreviewItem
}
}
}
As you can see, there's hardly any difference to my code from when I asked the question. Yesterday night, the fileURL was always nil for an unclear reason; yet, now it started working just fine. In exchange, the remote images in my list (not shown here) stopped working even though I haven't touched them, haha.
I don't know what's going on and what I even changed to make it work, but it works and I won't complain!