how to use UIViewRepresentable Coordinator delegate - swift

I'm using Pulley a maps drawer library which is written in UIKit in a SwiftUI project. I have a SwiftUI ListView that I'm using in the project via a UIHostingController but I want to disable scrolling when the drawers position is not open and to do that I'm pretty sure I need to use one of the delegate functions Pulley provides (drawerPositionDidChange) but I'm not sure how to use the delegate in the Coordinator or if I should even try to use the delegate, maybe I just need to use some type of state variable?
Delegate in the view controller
#objc public protocol PulleyDelegate: AnyObject {
/** This is called after size changes, so if you care about the bottomSafeArea property for custom UI layout, you can use this value.
* NOTE: It's not called *during* the transition between sizes (such as in an animation coordinator), but rather after the resize is complete.
*/
#objc optional func drawerPositionDidChange(drawer: PulleyViewController, bottomSafeArea: CGFloat)
}
This is the UIViewRepresentable where I'm trying to use the delegate.
import SwiftUI
struct DrawerPosition: UIViewControllerRepresentable {
#Binding var bottomSafeArea: CGFloat?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> some UIViewController {
let vc = PulleyViewController()
vc.delegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
// Updates the state of the specified view controller with new information from SwiftUI.
}
class Coordinator: NSObject, PulleyDrawerViewControllerDelegate {
var parent: DrawerPosition
init (_ parent: DrawerPosition) {
self.parent = parent
}
func drawerPositionDidChange(drawer: PulleyViewController, bottomSafeArea: CGFloat){
self.parent.bottomSafeArea = bottomSafeArea
}
}
}
the ListView where I want to disable the scroll.
import SwiftUI
struct ListView: View {
#State private var bottomSafeArea: CGFloat?
var body: some View {
ScrollViewReader { proxy in
VStack {
Button("Jump to #50") {
proxy.scrollTo(50)
}
List(0..<100, id: \.self) { i in
Text("Example")
.id(i)
}.scrollDisabled(bottomSafeArea == 0 ? true : false)
}
}
}
}
class ListViewVHC: UIHostingController<ListView> {
required init?(coder: NSCoder) {
super.init (coder: coder, rootView: ListView())
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}

Here is the correct way to set up a Coordinator:
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIViewController(context: Context) -> PullyViewController {
context.coordinator.pullyViewController
}
func updateUIViewController(_ uiViewController: PullyViewController, context: Context) {
// Updates the state of the specified view controller with new information from SwiftUI.
context.coordinator.bottomSafeAreaChanged = { bottomSafeArea in
self.bottomSafeArea = bottomSafeArea
}
}
class Coordinator: NSObject, PulleyDrawerViewControllerDelegate {
lazy var pullyViewController: PulleyViewController = {
let vc = PulleyViewController()
vc.delegate = self
return vc
}()
var bottomSafeAreaChanged: ((CGFloat) -> Void)?
func drawerPositionDidChange(drawer: PulleyViewController, bottomSafeArea: CGFloat){
bottomSafeAreaChanged?(bottomSafeArea)
}

Related

Use existing pages instead of initialised controllers in PageViewController

I'm using a PageViewController from this tutorial
Since the pages are being added as controllers, here:
controllers = parent.pages.map { UIHostingController(rootView: $0) }
Whenever they're updated in my SwiftUI view, the user can't see any updates, since (i'm guessing) the init method doesn't refresh.
So how am I able to use the current pages instead of instantiating a controllers array?
Here is my code:
PageViewController.swift
import SwiftUI
import UIKit
struct PageViewController<Page: View>: UIViewControllerRepresentable {
var pages: [Page]
var onLast: () -> AnyView
#Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .vertical)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
var controllers = [UIViewController]()
init(_ pageViewController: PageViewController) {
parent = pageViewController
controllers = parent.pages.map { UIHostingController(rootView: $0) }
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController?
{
guard let index = controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return controllers.last
}
return controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController?
{
guard let index = controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == controllers.count {
return controllers.first
}
return controllers[index + 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = controllers.firstIndex(of: visibleViewController) {
if index + 1 == controllers.count {
print("index: \(index)")
print(controllers.count)
controllers.append(UIHostingController(rootView: parent.onLast()))
}
parent.currentPage = index
}
}
}
}
struct PageView<Page: View>: View, Equatable {
static func == (lhs: PageView<Page>, rhs: PageView<Page>) -> Bool {
return true
}
var pages: [Page]
var onLast: () -> AnyView
#Binding var currentPage: Int
var body: some View {
PageViewController(pages: pages, onLast: onLast, currentPage: $currentPage)
}
}
SwiftUI View
import SwiftUI
struct Test: View {
#State var color: Color = Color.blue
var body: some View {
PageView(
pages: [
AnyView(
color
),
AnyView(
Color.purple
),
AnyView(Color.green),
AnyView(Color.orange),
],
onLast: {
color = Color.red
},
currentPage: $currentPage
)
.equatable()
}
}
Well, UIKit views should live in own world, SwiftUI views in own. Actually it is expected here that parent state will not update something deep inside bridged UIKit, even if it looks like it is in same stack, but actually it is just a function and it is called god-damn-known-when.
Anyway the approach is to solve it is simple - use SwiftUI tools to update SwiftUI views. Here it looks appropriate to use ObservableObject view model with explicit view observed it and work with that view model, because it is a reference-type.
Here is a sketch:
class ViewModel: ObservableObject {
#Published var color = Color.blue
}
struct Test: View {
#StateObject var vm = ViewModel() // << owner !!
var body: some View {
PageView(
pages: [
AnyView(
NativePath(vm: vm) // << inject
),
AnyView(
Color.purple
),
AnyView(Color.green),
AnyView(Color.orange),
],
onLast: {
vm.color = Color.red // << update by ref
},
currentPage: $currentPage
)
.equatable()
}
struct NativePath: View {
#ObservedObject var vm: ViewModel // << observe explicitly
var body: some View {
vm.color // << updated
}
}
}

Complex UIKit + SwiftUI interface via UIViewControllerRepresentable

I am building a camera app with all the UI in SwiftUI (parent) holding a UIKit Controller that contains all the recording functionalities. The UI is pretty complex, so would like if possible to remain with this structure for the project.
The UIKit Class has some functions like startRecord() stopRecord() which I would like to be triggered from the SwiftUI view. For that reason, I would like to 'call' the UIKit functions from my SwiftUI view.
I am experimenting with UIViewControllerRepresentable, being able to perform updates on a global variable change, but I am still not able to call the individual functions I want to trigger from the SwiftUI parent.
Here its the SwiftUI file:
init(metalView: MetalViewController?) {
self.metalView = MetalViewController(appStatus: appStatus)
}
var body: some View {
ZStack {
// - Camera view
metalView
.edgesIgnoringSafeArea(.top)
.padding(.bottom, 54)
VStack {
LateralMenuView(appStatus: appStatus, filterTooltipShowing: $_filterTooltipShowing)
Button("RECORD", action: {
print("record button pressed")
metalView?.myMetalDelegate.switchRecording(). // <-- Not sure about this
})
Here is the MetalViewController:
protocol MetalViewControllerDelegate {
func switchRecording()
}
// MARK: - The secret sauce for loading the MetalView (UIKit -> SwiftUI)
struct MetalViewController: UIViewControllerRepresentable {
var appStatus: AppStatus
typealias UIViewControllerType = MetalController
var myMetalDelegate: MetalViewControllerDelegate!
func makeCoordinator() -> Coordinator {
Coordinator(metalViewController: self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MetalViewController>) -> MetalController {
let controller = MetalController(appStatus: appStatus)
return controller
}
func updateUIViewController(_ controller: MetalController, context: UIViewControllerRepresentableContext<MetalViewController>) {
controller.changeFilter()
}
class Coordinator: NSObject, MetalViewControllerDelegate {
var controller: MetalViewController
init(metalViewController: MetalViewController) {
controller = metalViewController
}
func switchRecording() {
print("just testing")
}
}
}
and the UIKit Controller...
class MetalController: UIViewController {
var _mydelegate: MetalViewControllerDelegate?
...
override func viewDidLoad() {
...
self._mydelegate = self
}
extension MetalController: MetalViewControllerDelegate {
func switchRecording() {
print("THIS SHOULD BE WORKING, BUT ITS NOT")
}
}
I like to use Combine to pass messages through an ObservableObject to the UIKit views. That way, I can call them imperatively. Rather than trying to parse your code, I made a little example of the concept:
import SwiftUI
import Combine
enum MessageBridgeMessage {
case myMessage(parameter: Int)
}
class MessageBridge : ObservableObject {
#Published var result = 0
var messagePassthrough = PassthroughSubject<MessageBridgeMessage, Never>()
}
struct ContentView : View {
#StateObject private var messageBridge = MessageBridge()
var body: some View {
VStack {
Text("Result: \(messageBridge.result)")
Button("Add 2") {
messageBridge.messagePassthrough.send(.myMessage(parameter: messageBridge.result))
}
VCRepresented(messageBridge: messageBridge)
}
}
}
struct VCRepresented : UIViewControllerRepresentable {
var messageBridge : MessageBridge
func makeUIViewController(context: Context) -> CustomVC {
let vc = CustomVC()
context.coordinator.connect(vc: vc, bridge: messageBridge)
return vc
}
func updateUIViewController(_ uiViewController: CustomVC, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
class Coordinator {
private var cancellable : AnyCancellable?
func connect(vc: CustomVC, bridge: MessageBridge) {
cancellable = bridge.messagePassthrough.sink(receiveValue: { (message) in
switch message {
case .myMessage(let parameter):
bridge.result = vc.addTwo(input: parameter)
}
})
}
}
}
class CustomVC : UIViewController {
func addTwo(input: Int) -> Int {
return input + 2
}
}
In the example, MessageBridge has a PassthroughSubject that can be subscribed to from the UIKit view (or in this case, UIViewController). It's owned by ContentView and passed by parameter to VCRepresented.
In VCRepresented, there's a method on the Coordinator to subscribe to the publisher (messagePassthrough) and act on the messages. You can pass parameters via the associated properties on the enum (MessageBridgeMessage). Return values can be stored on #Published properties on the MessageBridge if you need them (or, you could setup another publisher to go the opposite direction).
It's a little verbose, but seems to be a pretty solid pattern for communication to any level of the tree you need (SwiftUI view, representable view, UIKit view, etc).

Passing data from simple NSView to SwiftUI View

I'm making a macOS app with SwiftUI. I have a struct conforming to NSViewRepresentable, whose purpose is to detect the key code of whichever key was pressed. I want to pass the event.keyCode back to SwiftUI and save it into an environment object so I can use the key code elsewhere in my app.
I know I am supposed to use SwiftUI coordinators, but all tutorials and Stack Overflow questions I can find use ready-made classes such as UIPageControl or UISearchBar that have pre-configured delegates. I'm not sure what to do when using a simple custom NSView. Can somebody explain how to pass the data from the NSViewRepresentable struct into my #EnvironmentObject when using a custom NSView?
struct KeyboardEvent: NSViewRepresentable {
class KeyView: NSView {
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
print("\(event.keyCode)")
}
}
func makeNSView(context: Context) -> NSView {
let view = KeyView()
DispatchQueue.main.async {
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
}
struct ContentView: View {
#EnvironmentObject var input: KeyboardInput // save the keyCode here
var body: some View {
VStack {
Text(input.keyCode)
KeyboardEvent()
}
}
}
Right now it prints the key code normally to the Xcode console, so the detection works fine.
Here is a solution (with some replicated parts). Tested with Xcode 11.4 / macOS 10.15.4
class KeyboardInput: ObservableObject {
#Published var keyCode: UInt16 = 0
}
struct KeyboardEvent: NSViewRepresentable {
#Binding var keyStorage: UInt16 // << here !!
init(into storage: Binding<UInt16>) {
_keyStorage = storage
}
class KeyView: NSView {
var owner: KeyboardEvent? // << view holder
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
print("\(event.keyCode)")
owner?.keyStorage = event.keyCode
}
}
func makeNSView(context: Context) -> NSView {
let view = KeyView()
view.owner = self // << inject
DispatchQueue.main.async {
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
}
struct ContentView: View {
#EnvironmentObject var input: KeyboardInput // save the keyCode here
var body: some View {
VStack {
Text("Code: \(input.keyCode)")
KeyboardEvent(into: $input.keyCode) // << binding !!!
}
}
}

How to present the STPPaymentOptionsViewController in swift ui

.sheet(isPresented: $showSheet) {
STPPaymentOptionsViewController()
}
I run this code hoping to present the Stripe Payment Options View Controller in my content view and I get this error:
Instance method sheet(isPresented:onDismiss:content:) requires that STPAddCardViewController conform to View
I also tried to wrap the view into a UIViewRepresentable like so:
struct PaymentOptionsView: UIViewRepresentable {
func makeUIView(context: Context) -> STPPaymentOptionsViewController {
let config = STPPaymentConfiguration()
config.additionalPaymentOptions = .default
config.requiredBillingAddressFields = .none
config.appleMerchantIdentifier = "dummy-merchant-id"
return STPPaymentOptionsViewController(configuration: config, e: STPTheme(), customerContext: STPCustomerContext(), delegate: self as! STPPaymentOptionsViewControllerDelegate)
}
}
Then I get the error:
Type CheckOut.PaymentOptionsView does not conform to protocol UIViewRepresentable.
Considering that STPPaymentOptionsViewController inherits from ViewController you need to use UIViewControllerRepresentable instead.
You also need to implement the required delegate methods for the STPPaymentOptionsViewControllerDelegate.
struct PaymentOptionsView: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, STPPaymentOptionsViewControllerDelegate {
var control: PaymentOptionsView
init(_ control: PaymentOptionsView) {
self.control = control
}
// Implement required delegate methods here:
func paymentOptionsViewControllerDidCancel(_ paymentOptionsViewController: STPPaymentOptionsViewController) {
}
func paymentOptionsViewControllerDidFinish(_ paymentOptionsViewController: STPPaymentOptionsViewController) {
}
func paymentOptionsViewController(_ paymentOptionsViewController: STPPaymentOptionsViewController, didFailToLoadWithError error: Error) {
}
}
func makeUIViewController(context: UIViewControllerRepresentableContext<PaymentOptionsView>) -> STPPaymentOptionsViewController {
let config = STPPaymentConfiguration()
config.additionalPaymentOptions = .default
config.requiredBillingAddressFields = .none
config.appleMerchantIdentifier = "dummy-merchant-id"
return STPPaymentOptionsViewController(configuration: config, theme: STPTheme(), apiAdapter: STPCustomerContext(), delegate: context.coordinator)
}
func updateUIViewController(_ uiViewController: STPPaymentOptionsViewController, context: UIViewControllerRepresentableContext<PaymentOptionsView>) { }
}
Keep in mind you're also setting the delegate in the STPPaymentOptionsViewController incorrectly. You need to use context.coordinator rather than self as! STPPaymentOptionsViewControllerDelegate.

Implement delegates within SwiftUI Views

I am trying to implement a functionality that requires a delegate method (like NSUserActivity). Therefore I need a UIViewController that conforms to NSUserActivityDelegate (or similar other delegates), handles and hold all the required information. My problem is that I am using SwiftUI for my interface and therefore I am not using UIViewControllers. So how can I implement this functionality and still use SwiftUI for the UI. What I tried: view1 is just a normal SwiftUI View that can present (via NavigationLink) view2 which is the view where in want to implement this functionality. So I tried instead of linking view1 and view2, linking view1 to a UIViewControllerRepresentable which then handles the implementation of this functionality and adds UIHostingController(rootView: view2) as a child view controller.
struct view1: View {
var body: some View {
NavigationLink(destination: VCRepresentable()) {
Text("Some Label")
}
}
}
struct view2: View {
var body: some View {
Text("Hello World!")
}
}
struct VCRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return implementationVC()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
}
class implementationVC: UIViewController, SomeDelegate for functionality {
// does implementation stuff in delegate methods
...
override func viewDidLoad() {
super.viewDidLoad()
attachChild(UIHostingController(rootView: view2()))
}
private func attachChild(_ viewController: UIViewController) {
addChild(viewController)
if let subview = viewController.view {
subview.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(subview)
subview.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
subview.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
subview.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
subview.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
viewController.didMove(toParent: self)
}
}
I am having trouble with transferring the data between my VC and my view2. So I'm wondering if there is a better way to implement such a functionality within a SwiftUI View.
You need to create a view that conforms to UIViewControllerRepresentable and has a Coordinator that handles all of the delegate functionality.
For example, with your example view controller and delegates:
struct SomeDelegateObserver: UIViewControllerRepresentable {
let vc = SomeViewController()
var foo: (Data) -> Void
func makeUIViewController(context: Context) -> SomeViewController {
return vc
}
func updateUIViewController(_ uiViewController: SomeViewController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(vc: vc, foo: foo)
}
class Coordinator: NSObject, SomeDelegate {
var foo: (Data) -> Void
init(vc: SomeViewController, foo: #escaping (Data) -> Void) {
self.foo = foo
super.init()
vc.delegate = self
}
func someDelegateFunction(data: Data) {
foo(data)
}
}
}
Usage:
struct ContentView: View {
var dataModel: DataModel
var body: some View {
NavigationLink(destination: CustomView(numberFromPreviousView: 10)) {
Text("Go to VCRepresentable")
}
}
}
struct CustomView: View {
#State var instanceData1: String = ""
#State var instanceData2: Data?
var numberFromPreviousView: Int // example of data passed from the previous view to this view, the one that can react to the delegate's functions
var body: some View {
ZStack {
SomeDelegateObserver { data in
print("Some delegate function was executed.")
self.instanceData1 = "Executed!"
self.instanceData2 = data
}
VStack {
Text("This is the UI")
Text("That, in UIKit, you would have in the UIViewController")
Text("That conforms to whatever delegate")
Text("SomeDelegateObserver is observing.")
Spacer()
Text(instanceData1)
}
}
}
}
Note: I renamed VCRepresentable to SomeDelegateObserver to be more indicative of what it does: Its sole purpose is to wait for delegate functions to execute and then run the closures (i.e foo in this example) you provide it. You can use this pattern to create as many functions as you need to "observe" whatever delegate functions you care about, and then execute code that can update the UI, your data model, etc. In my example, when SomeDelegate fires someDelegateFunction(data:), the view will display "Excuted" and update the data instance variable.