using qualtrics in a SwiftUI app with a UIViewControllerRepresentable - ios

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. 😃

Related

How to pass data from SwiftUI to ViewController

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
}
}

Pass variable from UIViewController to SwiftUI View

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()
}
}

SwiftUI with UIViewControllerRepresentable

I am trying to use a UIViewController representable in a swiftUi project. Specifically I am trying to press one button (assetOne) that allows the EU to select a video and then press another button (assetTwo) and it allows the user to select another video. Then the user will have the option to merge the videos (with a third button). I assumed that I would need to use a Coordinator to accomplish this but after seeing a SO solution without it I tried to do it without one. But when I run my project the build is successful but when I click on any of the buttons from the content view I get the error message below. What am I doing wrong? Do I need a Coordinator and how do I incorporate it with my current configuration?
Warning: Attempt to present <UIImagePickerController: 0x7fa05f827600>
on <TempTest.MergeVideoViewController: 0x7fa05ed088c0> whose view is
not in the window hierarchy!
Content View:
import SwiftUI
struct ContentView: View {
let someView = ImagePicker()
var body: some View {
VStack {
Button(action: {
print("SwiftUI: assetOne button tapped")
// Call func in SomeView()
self.someView.assetOne()
}) {
Text("Asset One").foregroundColor(Color.black)
}
.background(Color.blue)
.padding(10)
.clipShape(Capsule())
}
//...
ImagePicker: UIViewControllerRepresentable
struct ImagePicker: UIViewControllerRepresentable{
let someView = MergeVideoViewController()
func makeUIViewController(context: Context) -> MergeVideoViewController {
someView
}
func updateUIViewController(_ uiViewController: MergeVideoViewController, context: Context) {}
func assetOne() {
someView.loadAssetOne()
}
//...
}
My UIViewController class:
class MergeVideoViewController: UIViewController {
var firstAsset: AVAsset?
var secondAsset: AVAsset?
var audioAsset: AVAsset?
var loadingAssetOne = false
var activityMonitor: UIActivityIndicatorView!
func exportDidFinish(_ session: AVAssetExportSession) {
// Cleanup assets
activityMonitor.stopAnimating()
firstAsset = nil
secondAsset = nil
audioAsset = nil
//...
func loadAssetOne() {
// func loadAssetOne(_ sender: AnyObject) {
if savedPhotosAvailable() {
loadingAssetOne = true
VideoHelper.startMediaBrowser(delegate: self, sourceType: .savedPhotosAlbum)
}
}
//...
The ImagePicker is-a View, it should be somewhere in body.
Here is possible approach - the idea is to get controller reference back in SwiftUI and call its actions directly when needed.
struct ImagePicker: UIViewControllerRepresentable{
let configure: (MergeVideoViewController) -> ()
func makeUIViewController(context: Context) -> MergeVideoViewController {
let someView = MergeVideoViewController()
configure(someView)
return someView
}
func updateUIViewController(_ uiViewController: MergeVideoViewController, context: Context) {}
}
struct ContentView: View {
#State private var controller: MergeVideoViewController?
var body: some View {
VStack {
ImagePicker {
self.controller = $0
}
Button(action: {
print("SwiftUI: assetOne button tapped")
self.controller?.loadAssetOne()
}) {
Text("Asset One").foregroundColor(Color.black)
}
.background(Color.blue)
.padding(10)
.clipShape(Capsule())
}
}
}

How to change StatusBarStyle on SwiftUI App Lifecycle?

I already spent a lot of hours trying to figure out a way to change statusBarStyle to light/dark using the new lifecycle SwiftUI App.
The newest posts about the status bar teach how to hide it, but I don't want to do it, I just need to change it to dark or light.
To change the color, the most recent way I found is open SceneDelegate.swift and change window.rootViewController to use my own HostingController, but it will only work for projects using UIKit App Delegate Lifecycle. Using SwiftUI App Lifecycle, the SceneDelegate.swift will not be generated, so where can I do it?
I can do it via General Settings on the Xcode interface. My question is about how to do it via code dynamically.
Target: iOS 14
IDE: Xcode 12 beta 3
OS: MacOS 11 Big Sur
Below is what I got so far.
Everything.swift
import Foundation
import SwiftUI
class LocalStatusBarStyle { // style proxy to be stored in Environment
fileprivate var getter: () -> UIStatusBarStyle = { .default }
fileprivate var setter: (UIStatusBarStyle) -> Void = {_ in}
var currentStyle: UIStatusBarStyle {
get { self.getter() }
set { self.setter(newValue) }
}
}
struct LocalStatusBarStyleKey: EnvironmentKey {
static let defaultValue: LocalStatusBarStyle = LocalStatusBarStyle()
}
extension EnvironmentValues { // Environment key path variable
var localStatusBarStyle: LocalStatusBarStyle {
get {
return self[LocalStatusBarStyleKey.self]
}
}
}
class MyHostingController<Content>: UIHostingController<Content> where Content:View {
private var internalStyle = UIStatusBarStyle.default
#objc override dynamic open var preferredStatusBarStyle: UIStatusBarStyle {
get {
internalStyle
}
set {
internalStyle = newValue
self.setNeedsStatusBarAppearanceUpdate()
}
}
override init(rootView: Content) {
super.init(rootView:rootView)
LocalStatusBarStyleKey.defaultValue.getter = { self.preferredStatusBarStyle }
LocalStatusBarStyleKey.defaultValue.setter = { self.preferredStatusBarStyle = $0 }
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
struct TitlePage: View {
#Environment(\.localStatusBarStyle) var statusBarStyle
#State var title: String
var body: some View {
Text(title).onTapGesture {
if self.statusBarStyle.currentStyle == .darkContent {
self.statusBarStyle.currentStyle = .default
self.title = "isDefault"
} else {
self.statusBarStyle.currentStyle = .darkContent
self.title = "isDark"
}
}
}
}
struct ContainerView: View {
var controllers: [MyHostingController<TitlePage>]
init(_ titles: [String]) {
self.controllers = titles.map { MyHostingController(rootView: TitlePage(title: $0)) }
}
var body: some View {
PageViewController(controllers: self.controllers)
}
}
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
return pageViewController
}
func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) {
uiViewController.setViewControllers([controllers[0]], direction: .forward, animated: true)
}
typealias UIViewControllerType = UIPageViewController
}
MyApp.swift
import SwiftUI
#main
struct TestAppApp: App {
var body: some Scene {
WindowGroup {
ContainerView(["Subscribe", "Comment"])
}
}
}
struct TestAppApp_Previews: PreviewProvider {
static var previews: some View {
Text("Hello, World!")
}
}
Add two values to the Info.plist:
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
This works with the new SwiftUI App Lifecycle (#main). Verified on iOS14.4.
My suggestion is you just create an AppDelegate Adaptor and do whatever customization you need from there. SwiftUI will handle the creation of AppDelegate and managing its lifetime.
Create an AppDelegate class:
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UIApplication.shared.statusBarStyle = .darkContent
return true
}
}
Now inside your App:
#main
struct myNewApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
Text("I am a New View")
}
}
}
This is not really a solution but the best I could come up with (and ended up doing) was to force app to the dark mode. Either in Info.plist or NavigationView { ... }.preferredColorScheme(.dark)
That will also change the statusBar. You will not be able to change the status bar style per View though.
Before, with SceneDelegate
(code taken from SwiftUI: Set Status Bar Color For a Specific View)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
...
window.rootViewController = MyHostingController(rootView: contentView)
}
After, with no SceneDelegate
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
MyHostingController(rootView: ContentView())
}
}
}
If you follow the answer from the link above and apply it here with the #main, you should be able to achieve your changes.

Presenting UIDocumentInteractionController with UIViewControllerRepresentable in SwiftUI

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()
}
}
}

Resources