I can't find a way or a good tutorial to explain how to pass the value of a variable (String or Int) that is owned by a UIViewController to a SwiftUI view that is calling the view.
For example:
class ViewController: UIViewController {
var myString : String = "" // variable of interest
....
func methodThatChangeValueOfString(){
myString = someValue
}
}
// to make the view callable on SwiftUI
extension ViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = ViewController
public func makeUIViewController(context: UIViewControllerRepresentableContext<ViewController>) -> ViewController {
return ViewController()
}
func updateUIViewController(_ uiViewController: ViewController, context: UIViewControllerRepresentableContext<ViewController>) {
}
}
In SwiftUI I'll have
struct ContentView: View {
var body: some View {
ViewController()
}
}
How can I take myString of the ViewController and use it in ContentView?
Thanks in advance
Use MVVM pattern it is what is recommended with SwiftUI.
Share a ViewModel between your SwiftUI View and your UIKit ViewController.
I suggest you start with the basic Apple SwiftUI tutorials. Specifically how to "Interface with UIKit"
https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
import SwiftUI
struct SwiftUIView: View {
#StateObject var sharedVM: SharedViewModel = SharedViewModel()
var body: some View {
VStack{
UIKitViewController_UI(sharedVM: sharedVM)
Text(sharedVM.myString)
}
}
}
class SharedViewModel: ObservableObject{
#Published var myString = "init String"
}
//Not an extension
struct UIKitViewController_UI: UIViewControllerRepresentable {
typealias UIViewControllerType = UIKitViewController
var sharedVM: SharedViewModel
func makeUIViewController(context: Context) -> UIKitViewController {
return UIKitViewController(vm: sharedVM)
}
func updateUIViewController(_ uiViewController: UIKitViewController, context: Context) {
}
}
class UIKitViewController: UIViewController {
let sharedVM: SharedViewModel
var runCount = 0
init(vm: SharedViewModel) {
self.sharedVM = vm
super.init(nibName: nil, bundle: nil)
//Sample update mimics the work of a Delegate or IBAction, etc
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
self.runCount += 1
self.methodThatChangeValueOfString()
if self.runCount == 10 {
timer.invalidate()
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func methodThatChangeValueOfString(){
sharedVM.myString = "method change" + runCount.description
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
Related
I'm trying to make a simple swiftui app using qualtrics and I'm trying to use a uiviewrepresentable to make it work
#main
struct QualtricsPocApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
init() {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// i have the actual intercept id's here i just removed them
Qualtrics.shared.initializeProject(brandId: "brand", projectId: "proj", extRefId: "ref", completion: { (myInitializationResult) in print(myInitializationResult);})
return true
}
}
}
struct QualtricsViewRep: UIViewControllerRepresentable {
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: Context) -> UIViewController {
let vc = UIViewController()
Qualtrics.shared.evaluateProject { (targetingResults) in
for (interceptID, result) in targetingResults {
if result.passed() {
let displayed = Qualtrics.shared.display(viewController: self, autoCloseSurvey: true)
}
}
}
}
on let displayed = ... I keep getting the error "Cannot convert value of type 'QualtricsViewRep' to expected argument type 'UIViewController'", how can I return this code as a UIViewController to use in a swiftui app, or is there some other way I should be approaching this?
Unfortunately I don't have Qualtrics installed, but I have worked with it within UIKit. My assumption here is that you will need to create an instance of a UIViewController. This view controller is what the qualtrics view will present itself over.
Ultimately, you will return the view controller which contains the qualtrics view presented over it.
struct QualtricsViewRep: UIViewControllerRepresentable {
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: Context) -> UIViewController {
let vc = UIViewController()
Qualtrics.shared.evaluateProject { (targetingResults) in
for (interceptID, result) in targetingResults {
if result.passed() {
DispatchQueue.main.async {
let vc = UIViewController()
let displayed = Qualtrics.shared.display(viewController: vc, autoCloseSurvey: true)
}
}
}
}
return vc
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
// your code here
}
}
Sample Qualtrics SwiftUI App
I had to implement this for work so I decided to share my solution.
QualtricsDemoApp
import SwiftUI
import Qualtrics
#main
struct QualtricsDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
Qualtrics.shared.initializeProject(brandId: "BRAND_ID", projectId: "PROJECT_ID", extRefId: "EXT_REF_ID") { myInitializationResult in
print(myInitializationResult)
}
}
}
}
}
ContentView
import SwiftUI
import Qualtrics
struct ContentView: View {
#State private var showFeedback = false
var body: some View {
VStack {
Button("Show Qualtrics Feedback") {
showFeedback.toggle()
}
if showFeedback {
QualtricsFeedbackRepresentable()
}
}
.font(.title)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct QualtricsFeedbackRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let vc = UIViewController()
Qualtrics.shared.evaluateProject { (targetingResults) in
for (_, result) in targetingResults {
if result.passed() {
Qualtrics.shared.display(viewController: vc)
}
}
}
return vc
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
Now I just have to figure out why it's not in English. 😃
I am implementing a UIViewController with a ViewModel as an argument passed to the UIViewController, but I can't seem to make the delegate functions to work, what is the correct way of doing this?
CartView.swift
struct PaymentWrapper: UIViewControllerRepresentable {
typealias UIViewControllerType = CustomUIPaymentViewController
#ObservedObject var viewModel: CartViewModel
var vc: CustomUIPaymentViewController?
var foo: (String) -> Void
public init(viewModel: CartViewModel) {
self.viewModel = viewModel
self.vc = CustomUIPaymentViewController.init(token: self.viewModel.mtToken)
}
func makeUIViewController(context: Context) -> CustomUIPaymentViewController {
return vc!
}
func updateUIViewController(_ uiViewController: CustomUIPaymentViewController, context: Context) {
// code
}
func makeCoordinator() -> Coordinator {
Coordinator(vc: vc!, foo: foo)
}
class Coordinator: NSObject, CustomUIPaymentViewControllerDelegate, CustomUINavigationControllerDelegate {
var foo: (String) -> Void
init(vc: CustomUIPaymentViewController, foo: #escaping (String) -> Void) {
self.foo = foo
super.init()
vc.delegate = self
}
func paymentViewController(_ viewController: CustomUIPaymentViewController!, paymentFailed error: Error!) {
foo("FAILED")
}
func paymentViewController(_ viewController: CustomUIPaymentViewController!, paymentPending result: TransactionResult!) {
foo("PENDING")
}
func paymentViewController(_ viewController: CustomUIPaymentViewController!, paymentSuccess result: TransactionResult!) {
foo("SUCCESS")
}
func paymentViewController_paymentCanceled(_ viewController: CustomUIPaymentViewController!) {
foo("CANCEL")
}
//This delegate methods is added on ios sdk v1.16.4 to handle the new3ds flow
func paymentViewController(_ viewController: CustomUIPaymentViewController!, paymentDeny result: TransactionResult!) {
}
}
}
struct CartView: View {
#ObservedObject var viewModel = CartViewModel()
var body: some View {
VStack {
Header(title: "Cart", back: false)
}
.sheet(isPresented: $viewModel.showPayment) {
PaymentWrapper(viewModel: self.viewModel) { data in
print(data)
// This returns error "Extra trailing closure passed in call"
}
}
}
}
How do I get the delegate to work? what am I doing wrong? Thank you in advance.
hoping someone can help me with this, I am building an iOS app with Swift and SwiftUI that launches different Unity scenes with the click of different buttons. I have the Unity part working well thanks to some good tutorials but I am having trouble being able to change scene with different button clicks. The tutorials I have followed set up the Unity code in a view controller that is then launched from a navigation view. I can launch the view controller and change the sceneNumber to whichever one I want and it will load that scene. I can't find away to do that in a way that is set before I launch the view controller.
Here is my View where the LauncherView has two buttons that will launch the view controller, I would like to change which sceneNumber is sent to the ViewController. I also have the problem of when I go back in the app and click through again nothing is displaying. I think this is because the UnityBridge class is already created and in the background somewhere.
Any help would be greatly appreciated! Thanks.
Here is my view code:
var sceneNumber: String = ""
struct MyViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let vc = UIViewController()
UnityBridge.getInstance().onReady = {
print("Unity is now ready!")
UnityBridge.getInstance().show(controller: vc)
let api = UnityBridge.getInstance().api
api.test(sceneNumber)
}
return vc
}
func updateUIViewController(_ viewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View {
VStack{
MyViewController()
}
}
}
struct FirstView: View {
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: ContentView()) { //+ code to set sceneNumber to "1"
Text("Launch Unity Scene 1")
}
Text("")
NavigationLink(destination: ContentView()) { //+code to set sceneNumber to "2"
Text("Launch Unity Scene 2")
}
}
}
}
}
And here is the UnityBridge class code:
import Foundation
import UnityFramework
import SwiftUI
class API: NativeCallsProtocol {
internal weak var bridge: UnityBridge!
/**
Function pointers to static functions declared in Unity
*/
private var testCallback: TestDelegate!
/**
Public API
*/
public func test(_ value: String) {
self.testCallback(value)
}
/**
Internal methods are called by Unity
*/
internal func onUnityStateChange(_ state: String) {
switch (state) {
case "ready":
self.bridge.unityGotReady()
default:
return
}
}
internal func onSetTestDelegate(_ delegate: TestDelegate!) {
self.testCallback = delegate
}
}
class UnityBridge: UIResponder, UIApplicationDelegate, UnityFrameworkListener {
private static var instance : UnityBridge?
internal(set) public var isReady: Bool = false
public var api: API
private let ufw: UnityFramework
public var view: UIView? {
get { return self.ufw.appController()?.rootView }
}
public var onReady: () -> () = {}
public static func getInstance() -> UnityBridge {
print("Hello1.1")
//UnityBridge.instance?.unload()
if UnityBridge.instance == nil {
print("Hello1.2")
UnityBridge.instance = UnityBridge()
}
print("Hello1.3")
return UnityBridge.instance!
}
private static func loadUnityFramework() -> UnityFramework? {
print("Hello2")
let bundlePath: String = Bundle.main.bundlePath + "/Frameworks/UnityFramework.framework"
let bundle = Bundle(path: bundlePath)
if bundle?.isLoaded == false {
bundle?.load()
}
let ufw = bundle?.principalClass?.getInstance()
if ufw?.appController() == nil {
let machineHeader = UnsafeMutablePointer<MachHeader>.allocate(capacity: 1)
machineHeader.pointee = _mh_execute_header
ufw!.setExecuteHeader(machineHeader)
}
return ufw
}
internal override init() {
print("Hello3")
self.ufw = UnityBridge.loadUnityFramework()!
self.ufw.setDataBundleId("com.unity3d.framework")
self.api = API()
super.init()
self.api.bridge = self
self.ufw.register(self)
FrameworkLibAPI.registerAPIforNativeCalls(self.api)
ufw.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: nil)
}
public func show(controller: UIViewController) {
print("Hello4")
if self.isReady {
self.ufw.showUnityWindow()
}
if let view = self.view {
controller.view?.addSubview(view)
}
}
public func unload() {
print("Hello5")
self.ufw.unloadApplication()
}
internal func unityGotReady() {
print("Hello6")
self.isReady = true
onReady()
}
internal func unityDidUnload(_ notification: Notification!) {
print("Hello7")
ufw.unregisterFrameworkListener(self)
UnityBridge.instance = nil
}
}
I'm creating a new iOS app using SwiftUI where ever possible. However, I want to be able to generate a PDF with some data.
In a similar project without swiftUI I can do this
let docController = UIDocumentInteractionController.init(url: "PATH_TO_FILE")
docController.delegate = self
self.dismiss(animated: false, completion: {
docController.presentPreview(animated: true)
})
and as long as somewhere else in the view controller I have this:
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return self
}
I'm good to go.
What I can't work out is how to apply this to a UIViewControllerRepresentable and have it working in SwiftUI. Should my UIViewControllerRepresentable be aiming to be a UIViewController? How do I then set the delegate and presentPreview? Will this overlay any view and display full screen over my SwiftUI app as it does for my standard iOS app?
Thanks
Here is possible approach to integrate UIDocumentInteractionController for usage from SwiftUI view.
Full-module code. Tested with Xcode 11.2 / iOS 13.2
import SwiftUI
import UIKit
struct DocumentPreview: UIViewControllerRepresentable {
private var isActive: Binding<Bool>
private let viewController = UIViewController()
private let docController: UIDocumentInteractionController
init(_ isActive: Binding<Bool>, url: URL) {
self.isActive = isActive
self.docController = UIDocumentInteractionController(url: url)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPreview>) -> UIViewController {
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<DocumentPreview>) {
if self.isActive.wrappedValue && docController.delegate == nil { // to not show twice
docController.delegate = context.coordinator
self.docController.presentPreview(animated: true)
}
}
func makeCoordinator() -> Coordintor {
return Coordintor(owner: self)
}
final class Coordintor: NSObject, UIDocumentInteractionControllerDelegate { // works as delegate
let owner: DocumentPreview
init(owner: DocumentPreview) {
self.owner = owner
}
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return owner.viewController
}
func documentInteractionControllerDidEndPreview(_ controller: UIDocumentInteractionController) {
controller.delegate = nil // done, so unlink self
owner.isActive.wrappedValue = false // notify external about done
}
}
}
// Demo of possible usage
struct DemoPDFPreview: View {
#State private var showPreview = false // state activating preview
var body: some View {
VStack {
Button("Show Preview") { self.showPreview = true }
.background(DocumentPreview($showPreview, // no matter where it is, because no content
url: Bundle.main.url(forResource: "example", withExtension: "pdf")!))
}
}
}
struct DemoPDFPreview_Previews: PreviewProvider {
static var previews: some View {
DemoPDFPreview()
}
}
I ended up doing something like the following as I wasn't able to get this working reliably with UIViewControllerRepresentable and the above answer. You might need to edit / extend this for your usecase.
class DocumentController: NSObject, ObservableObject, UIDocumentInteractionControllerDelegate {
let controller = UIDocumentInteractionController()
func presentDocument(url: URL) {
controller.delegate = self
controller.url = url
controller.presentPreview(animated: true)
}
func documentInteractionControllerViewControllerForPreview(_: UIDocumentInteractionController) -> UIViewController {
return UIApplication.shared.windows.first!.rootViewController!
}
}
Usage:
struct DocumentView: View {
#StateObject var documentController = DocumentController()
var body: some View {
Button(action: {
documentController.presentDocument(url: ...)
}, label: {
Text("Show Doc")
})
}
}
Using QLPreviewController
I know the question is about UIDocumentInteractionController, but if you want to present a PDF file (for example), you can use a QLPreviewController.
Local file
Presenting a local file:
import SwiftUI
struct DocView: View {
#State private var buttonPressed: Bool = false
var body: some View {
Button {
buttonPressed = true
} label: {
Text("Show PDF file")
}
.sheet(isPresented: $buttonPressed) {
let localURL = Bundle.main.url(forResource: "Example", withExtension: "pdf")!
PreviewController(url: localURL)
}
}
}
Remote file
Please see this gist if you need to present a remote file.
PreviewController
The UIViewControllerRepresentable for QLPreviewController.
import QuickLook
import SwiftUI
struct PreviewController: UIViewControllerRepresentable {
#Environment(\.dismiss) private var dismiss
let url: URL
func makeUIViewController(context: Context) -> UINavigationController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
controller.navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .done, target: context.coordinator,
action: #selector(context.coordinator.dismiss)
)
let navigationController = UINavigationController(rootViewController: controller)
return navigationController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: QLPreviewControllerDataSource {
let parent: PreviewController
init(parent: PreviewController) {
self.parent = parent
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(
_ controller: QLPreviewController,
previewItemAt index: Int
) -> QLPreviewItem {
return parent.url as NSURL
}
#objc func dismiss() {
parent.dismiss()
}
}
}
I'm quite new to Swift and absolute new to SwiftUI.
I'm trying to combine UIViewController Google Maps and modern SwiftUI.
In SwiftUI i have a few Text objects, and I want my GmapsController class to be able to modify these Text values, and redraw SwiftUI struct.
My main struct:
var _swiftUIStruct : swiftUIStruct = swiftUIStruct() // to have access in GmapsController
struct GoogMapView: View {
var body: some View {
let gmap = GmapsControllerRepresentable()
return ZStack {
gmap
_swiftUIStruct
}
}
}
UIViewControllerRepresentable wrapper of GmapsController :
struct GmapsControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<GmapsControllerRepresentable>) -> GmapsController {
return GmapsController()
}
func updateUIViewController(_ uiViewController: GmapsController, context: UIViewControllerRepresentableContext<GmapsControllerRepresentable>) {}
}
The GmapsController itself:
class GmapsController: UIViewController, CLLocationManagerDelegate, GMSMapViewDelegate {
var locationManager = CLLocationManager()
var mapView: GMSMapView!
override func viewDidLoad() {
super.viewDidLoad()
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
let camera = GMSCameraPosition.camera(
withLatitude: (locationManager.location?.coordinate.latitude)!,
longitude: (locationManager.location?.coordinate.longitude)!,
zoom: 15
)
mapView = GMSMapView.map(withFrame: view.bounds, camera: camera)
mapView.delegate = self
self.view = mapView
}
// HERE I WANT TO CHANGE SOME swiftUIStruct Text FIELDS
// calls after map move
func mapView(_ mapView: GMSMapView, idleAt сameraPosition: GMSCameraPosition) {
_swiftUIStruct.someTxt = "I hope this will work" // compilation pass, but value doesn't change
}
}
And the swiftUIStruct struct:
struct swiftUIStruct {
#State var someTxt = ""
var body: some View {
Text(someTxt) // THE TEXT I WISH I COULD CHANGE
}
}
Googling this a whole day just made me feel dumb, any help is appreciated.
I hope my example code helps. Basically, move the model data outside, and pass it along, and change it. If you run this code, you will see the text "I hope this will work", NOT "Initial Content".
import SwiftUI
class ViewModel: ObservableObject {
#Published var someTxt = "Initial Content"
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
ZStack {
GoogleMapsView(viewModel: viewModel)
Text(viewModel.someTxt)
}
}
}
struct GoogleMapsView: UIViewControllerRepresentable {
var viewModel: ViewModel
func makeUIViewController(context: Context) -> GmapsController {
let controller = GmapsController()
controller.viewModel = viewModel
return controller
}
func updateUIViewController(_ uiViewController: GmapsController, context: Context) {}
}
class GmapsController: UIViewController {
var viewModel: ViewModel?
override func viewDidLoad() {
viewModel?.someTxt = "I hope this will work"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return ContentView()
}
}