Pass #Published properties from view controllers to SwiftUI - ios

Suppose you have a legacy view controller that I'd like to use with SwiftUI. The view controller has one #Published property that contains it current state:
class LegacyViewController: UIViewController {
enum State {
case opened
case closed
case halfOpened
}
#Published var state: State
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
self.state = .closed
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
// state is changed after some time
}
}
Ideally, I'd like to use it with SwiftUI like this:
struct ContentView: View {
#State var state: LegacyViewController.State
var body: some View {
VCWrapper(state: $state).overlay (
Text("\(state)")
)
}
}
which would mean that I need to implement UIViewControllerRepresentable protocol:
struct VCWrapper: UIViewControllerRepresentable {
#Binding var state: LegacyViewController.State
func makeUIViewController(context: Context) -> LegacyViewController {
let vc = LegacyViewController(nibName: nil, bundle: nil)
/// where to perform the actual binding?
return vc
}
func updateUIViewController(_ uiViewController: LegacyViewController, context: Context) {
}
}
However, I'm having trouble figuring out where to do the actual binding from state property of the LegacyViewController to the state property exposed by VCWrapper. If LegacyViewController exposed a delegate, I could implement the binding through the Coordinator object, but I'm not so sure how to do this considering that I don't use a delegate object?

Here is possible solution - use Combine. Tested with Xcode 12 / iOS 14.
import Combine
struct VCWrapper: UIViewControllerRepresentable {
#Binding var state: LegacyViewController.State
func makeUIViewController(context: Context) -> LegacyViewController {
let vc = LegacyViewController(nibName: nil, bundle: nil)
// subscribe to controller state publisher and update bound
// external state
context.coordinator.cancelable = vc.$state
.sink {
DispatchQueue.main.async {
_state.wrappedValue = $0
}
}
return vc
}
func updateUIViewController(_ uiViewController: LegacyViewController, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator {
var cancelable: AnyCancellable?
}
}

Related

using qualtrics in a SwiftUI app with a UIViewControllerRepresentable

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

SwiftUI UIViewRepresentable with argument passed and delegate function

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.

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

Is there a SwiftUI equivalent for viewWillDisappear(_:) or detect when a view is about to be removed?

In SwiftUI, I'm trying to find a way to detect that a view is about to be removed only when using the default navigationBackButton. Then perform some action.
Using onDisappear(perform:) acts like viewDidDisappear(_:), and the action performs after another view appears.
Or, I was thinking the above problem might be solved by detecting when the default navigationBarBackButton is pressed. But I've found no way to detect that.
Is there any solution to perform some action before another view appears?
(I already know it is possible to do that by creating a custom navigation back button to dismiss a view)
Here is approach that works for me, it is not pure-SwiftUI but I assume worth posting
Usage:
SomeView()
.onDisappear {
print("x Default disappear")
}
.onWillDisappear { // << order does NOT matter
print(">>> going to disappear")
}
Code:
struct WillDisappearHandler: UIViewControllerRepresentable {
func makeCoordinator() -> WillDisappearHandler.Coordinator {
Coordinator(onWillDisappear: onWillDisappear)
}
let onWillDisappear: () -> Void
func makeUIViewController(context: UIViewControllerRepresentableContext<WillDisappearHandler>) -> UIViewController {
context.coordinator
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<WillDisappearHandler>) {
}
typealias UIViewControllerType = UIViewController
class Coordinator: UIViewController {
let onWillDisappear: () -> Void
init(onWillDisappear: #escaping () -> Void) {
self.onWillDisappear = onWillDisappear
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
onWillDisappear()
}
}
}
struct WillDisappearModifier: ViewModifier {
let callback: () -> Void
func body(content: Content) -> some View {
content
.background(WillDisappearHandler(onWillDisappear: callback))
}
}
extension View {
func onWillDisappear(_ perform: #escaping () -> Void) -> some View {
self.modifier(WillDisappearModifier(callback: perform))
}
}
You can bind the visibility of the child view to some state, and monitor that state for changes.
When the child view is pushed, the onChange block is called with show == true. When the child view is popped, the same block is called with show == false:
struct ParentView: View {
#State childViewShown: Bool = false
var body: some View {
NavigationLink(destination: Text("child view"),
isActive: self.$childViewShown) {
Text("show child view")
}
.onChange(of: self.childViewShown) { show in
if show {
// child view is appearing
} else {
// child view is disappearing
}
}
}
}
Here's a slightly more succinct version of the accepted answer:
private struct WillDisappearHandler: UIViewControllerRepresentable {
let onWillDisappear: () -> Void
func makeUIViewController(context: Context) -> UIViewController {
ViewWillDisappearViewController(onWillDisappear: onWillDisappear)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
private class ViewWillDisappearViewController: UIViewController {
let onWillDisappear: () -> Void
init(onWillDisappear: #escaping () -> Void) {
self.onWillDisappear = onWillDisappear
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
onWillDisappear()
}
}
}
extension View {
func onWillDisappear(_ perform: #escaping () -> Void) -> some View {
background(WillDisappearHandler(onWillDisappear: perform))
}
}
you have a couple of actions for each object that you want to show on the screen
func onDisappear(perform action: (() -> Void)? = nil) -> some View
//Adds an action to perform when this view disappears.
func onAppear(perform action: (() -> Void)? = nil) -> some View
//Adds an action to perform when this view appears.
you can use the like the sample ( in this sample it affects on the VStack):
import SwiftUI
struct TestView: View {
#State var textObject: String
var body: some View {
VStack {
Text(textObject)
}
.onAppear {
textObject = "Vertical stack is appeared"
}
.onDisappear {
textObject = ""
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
TestView()
}
}
}
You can trigger the change of the #Environment .scenePhase like this :
struct YourView: View {
#Environment(\.scenePhase) var scenePhase
var body: Some View {
VStack {
// Your View code
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
print("active")
case .inactive:
print("inactive")
case .background:
print("background")
#unknown default:
print("?")
}
}
}
}

Resources