How to authenticate with the Box SDK using SwiftUI? - swift

I have been having a hard time trying to figure out how to authenticate with the Box API using SwiftUI.
As far as I understand, SwiftUI does not currently have the ability to satisfy the ASWebAuthenticationPresentationContextProviding protocol required to show the Safari OAuth2 login sheet. I know that I can make a UIViewControllerRepresentable to use UIKit within SwiftUI, but I can't get this to work.

I have figured out how to get the OAuth2 login sheet for Dropbox to appear and authenticate the client using SwiftUI.
The trick is to use a Coordinator to make the UIViewControllerRepresentable satisfy a protocol.
import SwiftUI
import BoxSDK
import AuthenticationServices
var boxSDK = BoxSDK(clientId: "<Client ID>", clientSecret: "<Client Secret>")
var boxClient: BoxClient
struct BoxLoginView: View {
#State var showLogin = false
var body: some View {
VStack {
Button {
showLogin = true
} label: {
Text("Login")
}
BoxView(isShown: $showLogin)
// Arbitrary frame size so that this view does not take up the whole screen
.frame(width: 40, height: 40)
}
}
}
/// A UIViewController that will present the OAuth2 Safari login screen when the isShown is true.
struct BoxView: UIViewControllerRepresentable {
typealias UIViewControllerType = UIViewController
let letsView = UIViewController()
#Binding var isShown : Bool
// Show the login Safari window when isShown
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
if(isShown) {
getOAuthClient()
}
}
func makeUIViewController(context _: Self.Context) -> UIViewController {
return self.letsView
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
func getOAuthClient() {
boxSDK.getOAuth2Client(tokenStore: KeychainTokenStore(), context:self.makeCoordinator()) { result in
switch result {
case let .success(client):
boxClient = client
case let .failure(error):
print("error in getOAuth2Client: \(error)")
}
}
}
class Coordinator: NSObject, ASWebAuthenticationPresentationContextProviding {
var parent: BoxView
init(parent: BoxView) {
self.parent = parent
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return parent.letsView.view.window ?? ASPresentationAnchor()
}
}
}

Related

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 use UIViewRepresentable Coordinator delegate

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

Admob rewarded ad won't load

I have build a SwiftUI .sheet() that appears so the user can see their statistics and can click on a button that then will show a rewarded video to give the user some extra points. The code is as follows:
import SwiftUI
import GoogleMobileAds
struct Scoresheet: View {
#State private var rewardedadd = ViewController()
var body: some View {
VStack {
// all sorts of content.
// if a user clicks on a specific button it will call func loadadd()
}
.onAppear {
rewardedadd.loadRewardedAd()
}
}
func loadadd() {
rewardedadd.show()
}
}
In another file I have the following:
import UIKit
import GoogleMobileAds
class ViewController: UIViewController, GADFullScreenContentDelegate {
var rewardedAd: GADRewardedAd?
func loadRewardedAd() {
let request = GADRequest()
GADRewardedAd.load(withAdUnitID:"ca-app-pub-3940256099942544/1712485313",
request: request,
completionHandler: { [self] ad, error in
if let error = error {
print("Failed to load rewarded ad with error: \(error.localizedDescription)")
return
}
rewardedAd = ad
print("Rewarded ad loaded.")
rewardedAd?.fullScreenContentDelegate = self
})
}
func show() {
if let ad = rewardedAd {
ad.present(fromRootViewController: self) {
let reward = ad.adReward
print("Reward received with currency \(reward.amount), amount \(reward.amount.doubleValue)")
// TODO: Reward the user.
}
} else {
print("Ad wasn't ready")
}
}
func ad(_ ad: GADFullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
print("Ad did fail to present full screen content. Error: \(error.localizedDescription)")
}
func adWillPresentFullScreenContent(_ ad: GADFullScreenPresentingAd) {
print("Ad will present full screen content.")
}
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
print("Ad did dismiss full screen content.")
}
}
I keep getting the following error:
Ad did fail to present full screen content. Error: The provided view controller is not being presented.
That is the error from func ad in class ViewController. Does anyone know what I can do to fix this issue?
---- edit ----
I have rebuild my viewcontroller class to the following:
import SwiftUI
import GoogleMobileAds
struct RewardedAd: UIViewRepresentable {
#State private var rewardedAd: GADRewardedAd
let adUnitId = "ca-app-pub-3940256099942544/1712485313"
func makeCoordinator() -> CoordinatorVideoAd {
return CoordinatorVideoAd()
}
func makeUIView(context: Context) -> GADRewardedAd {
let request = GADRequest()
rewardedAd.load(withAdUnitID: adUnitId,
request: request, completionHandler: { [self] ad, error in
if let error = error {
print("Failed to load rewarded ad with error: \(error.localizedDescription)")
return
}
rewardedAd = ad
print("Rewarded ad loaded.")
rewardedAd?.fullScreenContentDelegate = self
})
return rewardedAd
}
func updateUIView(_ uiView: GADRewardedAd, context: Context) { }
class CoordinatorVideoAd: NSObject, GADFullScreenContentDelegate {
func ad(_ ad: GADFullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
print("Ad did fail to present full screen content.")
}
func adWillPresentFullScreenContent(_ ad: GADFullScreenPresentingAd) {
print("Ad will present full screen content.")
}
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
print("Ad did dismiss full screen content.")
}
}
}
I new have two error's:
Type 'RewardedAd' does not conform to protocol 'UIViewRepresentable'
Static member 'load' cannot be used on instance of type 'GADRewardedAd'
How can I fix this?
I believe that's now how you present a UIKit component to a swiftUI. You should read it in the apple docs about how to do it.
Here's an example how to do it :
First, create a struct that inherit the UIViewControllerRepresentable protocol
it'll look like this
struct SomethingViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = [enter your UIViewController type]
}
add the type, for example, here i;m creating a FakeViewController
typealias UIViewControllerType = FakeViewController
it'll generate the nescessary method
Last, just customize it whahtever you want
struct SomethingViewController: UIViewControllerRepresentable {
#Binding var colorIDX: Int
let colors: [UIColor] = [.red,.blue,.green]
func makeUIViewController(context: Context) -> FakeViewController {
//Construct your ViewController
let viewController = FakeViewController()
viewController.bgColor = .systemPink
return viewController
}
func updateUIViewController(_ uiViewController: FakeViewController, context: Context) {
// Update your ViewController
uiViewController.view.backgroundColor = colors[colorIDX]
}
}
and setup in the SwiftUIView to look like this (depending on how do you want it, navigate to UIViewController or present it)
struct ContentView: View {
#State var isShow: Bool = false
#State var colorIDX: Int = 0
var body: some View {
VStack {
Button("show") {
if colorIDX >= 2 {
colorIDX = 0
} else {
colorIDX += 1
}
isShow.toggle()
}
}.sheet(isPresented: $isShow) {
} content: {
SomethingViewController(colorIDX: $colorIDX)
}
}
}
you see, every time i update something in the swiftUI View, it updates my ViewController and everytime I push the button, it'll present the ViewController
if you want to use a navigationController, just use it like any other swiftUI component/views
And that;s how you use a UIViewController in SwiftUI

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

SwiftUI using NSSharingServicePicker in MacOS

I am trying to use a Share function inside my MacOS app in SwiftUI. I am having a URL to a file, which I want to share. It can be images/ documents and much more.
I found NSSharingServicePicker for MacOS and would like to use it. However, I am struggeling to use it in SwiftUI.
Following the documentation, I am creating it like this:
let shareItems = [...]
let sharingPicker : NSSharingServicePicker = NSSharingServicePicker.init(items: shareItems as [Any])
sharingPicker.show(relativeTo: NSZeroRect, of:shareView, preferredEdge: .minY)
My problem is in that show() method. I need to set a NSRect, where I can use NSZeroRect.. but I am struggeling with of: parameter. It requires a NSView. How can I convert my current view as NSView and use it that way. Or can I use my Button as NSView(). I am struggling with that approach.
Another option would be to use a NSViewRepresentable. But should I just create a NSView and use it for that method.
Here is minimal working demo example
struct SharingsPicker: NSViewRepresentable {
#Binding var isPresented: Bool
var sharingItems: [Any] = []
func makeNSView(context: Context) -> NSView {
let view = NSView()
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
if isPresented {
let picker = NSSharingServicePicker(items: sharingItems)
picker.delegate = context.coordinator
// !! MUST BE CALLED IN ASYNC, otherwise blocks update
DispatchQueue.main.async {
picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(owner: self)
}
class Coordinator: NSObject, NSSharingServicePickerDelegate {
let owner: SharingsPicker
init(owner: SharingsPicker) {
self.owner = owner
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) {
// do here whatever more needed here with selected service
sharingServicePicker.delegate = nil // << cleanup
self.owner.isPresented = false // << dismiss
}
}
}
Demo of usage:
struct TestSharingService: View {
#State private var showPicker = false
var body: some View {
Button("Share") {
self.showPicker = true
}
.background(SharingsPicker(isPresented: $showPicker, sharingItems: ["Message"]))
}
}
Another option without using NSViewRepresentable is:
extension NSSharingService {
static func submenu(text: String) -> some View {
return Menu(
content: {
ForEach(items, id: \.title) { item in
Button(action: { item.perform(withItems: [text]) }) {
Image(nsImage: item.image)
Text(item.title)
}
}
},
label: {
Image(systemName: "square.and.arrow.up")
}
)
}
}
You lose things like the "more" menu item or recent recipients. But in my opinion it's more than enough, simple and pure SwiftUI.