I'm trying to integrate Snapkit with an iOS app but I want to use SwiftUI instead of UIKit. I've already done the required setup with Snapkit and now I'm trying to get the snapchat login button to display in my app. I know the Snapkit SDK is made for UIKit and not SwiftUI, but SwiftUI has a way to wrap UIViews into SwiftUI with the UIViewRepresentable protocol. I've tried implementing this but the login button still doesn't display.
Here's my code:
import SwiftUI
import UIKit
import SCSDKLoginKit
struct ContentView: View {
var body: some View {
SnapchatLoginButtonView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct SnapchatLoginButtonView: UIViewRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> SCSDKLoginButton {
let s = SCSDKLoginButton()
s.delegate = context.coordinator
return s
}
func updateUIView(_ uiView: SCSDKLoginButton, context: Context) {
}
class Coordinator: NSObject, SCSDKLoginButtonDelegate {
func loginButtonDidTap() {
}
}
}
I have a feeling I'm missing something from within SCSDKLoginButton, but not sure what it is so here's the file SCSDKLoginButton.h for reference. Any help would be greatly appreciated!
//
// SCSDKLoginButton.h
// SCSDKLoginKit
//
// Copyright © 2018 Snap, Inc. All rights reserved.
//
#import <UIKit/UIKit.h>
#protocol SCSDKLoginButtonDelegate
- (void)loginButtonDidTap;
#end
#interface SCSDKLoginButton : UIView
#property (nonatomic, weak, nullable) id<SCSDKLoginButtonDelegate> delegate;
- (instancetype)initWithCompletion:(nullable void (^)(BOOL success, NSError *error))completion NS_DESIGNATED_INITIALIZER;
#end
Coincidentally I attempted implementing the SnapKit SDK in an exclusive SwiftUI/ iOS13 project about 3 days after you posted your Issue.
Unfortunately I can't directly resolve your issue as there are a few key issues which Snapchat has to address with their SDK before it is suitable for development with the SceneDelegate & AppDelegate Paradigm introduced in iOS 13. But I hope I can shed light on your question and present my findings to anyone else who is in a similar predicament.
These are the following issues/ observations I made on my quest of Implementing SCSDKLoginKit & SCSDKBitmojiKit in SwiftUI:
The most basic issue is that the SCSDKLoginKit Module is outdated, As you correctly realised. SCSDKLoginClient.login() requires the calling view to conform to the (UIKIT) UIViewController class. So we must use the workaround with a UIViewControllerRepresentable to act as our SwiftUI <-> UIKit intermediary.
However the fundamental issue relates to the fact that the SnapKit SDK documentation has not been updated to give developers a SceneDelegate link between Snapchat Auth and Your app's logic. So even if you implemented your SCSDKLoginButton correctly it is not smooth sailing!
Now to directly answer your question, You are attempting to wrap a SCSDKLoginButton in a UIViewControllerRepresentable which can be done and I'm sure someone with better knowledge of coordinators etc than myself can help you with that. However I just wanted to show that your efforts at the moment may be fruitless until snapchat provides an updated SDK.
Here is my setup:
[ContentView.swift]
import SwiftUI
struct ContentView: View {
#State private var isPresented = false
var body: some View {
Button("Snapchat Login Button") { self.isPresented = true}
.sheet(isPresented: $isPresented) {
LoginCVWrapper()
}
}
}
[LoginCVWrapper.swift]
import SwiftUI
import UIKit
import SCSDKLoginKit
struct LoginCVWrapper: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return LoginViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
//Unused in demonstration
}
}
[LoginViewController.swift]
import UIKit
import SCSDKLoginKit
class LoginViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
performLogin() //Attempt Snap Login Here
}
//Snapchat Credential Retrieval Fails Here
private func performLogin() {
//SCSDKLoginClient.login() never completes once scene becomes active again after Snapchat redirect back to this app.
SCSDKLoginClient.login(from: self, completion: { success, error in
if let error = error {
print("***ERROR LOC: manualTrigger() \(error.localizedDescription)***")
return
}
if success {
self.fetchSnapUserInfo({ (userEntity, error) in
print("***SUCCESS LOC: manualTrigger()***")
if let userEntity = userEntity {
DispatchQueue.main.async {
print("SUCCESS:\(userEntity)")
}
}
})
}
})
}
private func fetchSnapUserInfo(_ completion: #escaping ((UserEntity?, Error?) -> ())){
let graphQLQuery = "{me{displayName, bitmoji{avatar}}}"
SCSDKLoginClient
.fetchUserData(
withQuery: graphQLQuery,
variables: nil,
success: { userInfo in
if let userInfo = userInfo,
let data = try? JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted),
let userEntity = try? JSONDecoder().decode(UserEntity.self, from: data) {
completion(userEntity, nil)
}
}) { (error, isUserLoggedOut) in
completion(nil, error)
}
}
}
[This Runs as follows]:
GIF: Code Running On Device
More on the SceneDelegate interface link issue:
When you inevitably implement the SCSDKLoginClient.login() call (presumably when your SCSDKLoginButton is pressed), Snapchat will open, it will present the "grant access" sheet correctly assuming your app is linked in the Snapchat Dev Portal.
When you accept these permissions, Snapchat redirects to your application. However this is where the link between your app and retrieving a snapchat username/ bitmoji will breakdown. This is due to the fact that in new iOS 13 apps, the SceneDelegate handles when your application states change not the AppDelegate as in pre iOS13 versions. Therefore Snapchat returns the user data but your app never retrieves it.
[Going Forward]
The SnapKitSDK (currently version 1.4.3) needs to be updated along with the documentation.
I have just submitted a support question to Snapchat asking when this update will come so I will update this if I hear more. Apologies if you were looking for a direct solution to your SCSDKLoginButton() issue, I just wanted you to know what challenges lie beyond it at this current moment in time.
[Further Reading]
Facebook has updated their tools and documentation to incorporate the Scene/App Delegates. See step "5. Connect Your App Delegate and Scene Delegate" here: https://developers.facebook.com/docs/facebook-login/ios/
#Stephen2697 was right in pointing out that the snap sdk isn't built for iOS 13 yet due to SceneDelegate now handling oauth redirects rather than AppDelegate. I figured out a workaround to use the SCSDKLoginClient.application() method (which was made for appdelegate) to be used in scene delegate. Here's the code, add it to your scene delegate and the completion handler passed in to your Snapchat login will run:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for urlContext in URLContexts {
let url = urlContext.url
var options: [UIApplication.OpenURLOptionsKey : Any] = [:]
options[.openInPlace] = urlContext.options.openInPlace
options[.sourceApplication] = urlContext.options.sourceApplication
options[.annotation] = urlContext.options.annotation
SCSDKLoginClient.application(UIApplication.shared, open: url, options: options)
}
}
As far as the login button not appearing, make sure to add a completion handler to the SCSDKLoginButton instance or else it won't work. Like this:
let s = SCSDKLoginButton { (success, error) in
//handle login
}
To follow up with #Stephen.Alger's answer, you don't have to use UIViewControllerRepresentable just call: SCSDKLoginClient.login directly from SwiftUI's Button callback closure and add this modifier to the view:
.onOpenURL(perform: { url in
let success = SCSDKLoginClient.application(UIApplication.shared, open: url)
if success {
print("Logged in")
}
})
Related
I am trying to coordinate my deep link with push notifications so they both process my custom url scheme in the same manner and navigate to the appropriate view. The challenge seems to be with push notifications and how to process the link passed, through an apn from Azure Notification Hubs, using the same #EnvironmentObject that the onOpenUrl uses without breaking the SwiftUI paradigm and using a singleton.
Here is how I trigger the notification on my simulator, which works fine and navigates me to the appropriate view:
xcrun simctl openurl booted "myapp://user/details/123456"
Which triggers this the onOpenUrl in this code:
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(sessionInfo)
.onOpenURL { url in
print("onOpenURL: \(url)")
if sessionInfo.processDeepLink(url: url) {
print("deep link TRUE")
} else {
print("deep link FALSE")
}
}
}
}
And all my DeepLinks work just as desired. I wanted to trigger them from a notification so I created an apns file with the same link that worked using xcrun:
{
"aps": {
"alert": { // alert data },
"badge": 1,
"link_url":"myapp://user/details/123456"
}
}
and pushed it to the simulator like this:
xcrun simctl push xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx com.myco.myapp test.apn
How do I reference my object from the AppDelegate which gets the message:
func notificationHub(_ notificationHub: MSNotificationHub!, didReceivePushNotification message: MSNotificationHubMessage!) {
print("notificationHub...")
let userInfo = ["message": message!]
print("user: ")
NotificationCenter.default.post(name: NSNotification.Name("MessageReceived"), object: nil, userInfo: userInfo)
if (UIApplication.shared.applicationState == .background) {
print("Notification received in the background")
} else {
print("Notification received in the foreground")
}
UIApplication.shared.applicationIconBadgeNumber = 4
}
I looked at this post, but couldn't relate the components to my app, possibly due to the NotificationHub part of it. I also saw this post, but again didn't know how to connect it.
I saw this post and it looks like push notification and deep linking are two different things. I could use the same logic if I could access the SessionInfo object from the AppDelegate. I'm concerned about messing around in there given I'm new to iOS development. Here is what I'm using in the App:
#StateObject var sessionInfo: SessionInfo = SessionInfo()
This post seems to cover it but I'm still having trouble. I don't know what this means:
static let shared = AppState()
And my SessionInfo is decorated with #MainActor. When I access it from other places I use:
#EnvironmentObject var sessionInfo: SessionInfo
I also saw this post, which there was no selected answer, but the one which did exist seemed to recommend making the AppDelegate and EnvrionmentObject and push it into the ContentView. I think what I really need is the AppDelegate when the notification arrives to update something shared/published to the SessionInfo object so the url is parsed and the navigation kicked off. This seems backwards to me.
This post makes the AppDelegate an ObservableObject with a property which is published and makes the AppDelegate an EnvrionmentObject, so when the value is updated, it's published. If it were the navigation link/object that would work but something would still need to process it and it would not make sense for the onOpenUrl to use the AppDelegate, so again I think this is backwards.
If I did follow the post where there is a static SessionInfo object in the SessionInfo class, singleton, that means I would need to remove the #EnvironmentObject var sessionInfo: SessionInfo from the ContentView and the .environmentObject(sessionInfo) on the main View I am using I think and instead instantiate the shared object in each view where it is used. Right? It seems like I followed this whole #EnvrionmentObject, #StateObject, #MainActor paradigm and would have to abandon it. I'm not sure if that is right or what the tradeoffs are.
Most recently this post seems to be pretty in-depth, but introduces a new element, UNUserNotificationCenter, which I heard referenced in this youtube video.
This article was very helpful for the notification part.
Azure NotificationHubs the message info is in message.userInfo["aps"] vs userInfo["aps"] in the example or most places I have seen it. Not much documentation on MSNotificationHubMessage:
func notificationHub(_ notificationHub: MSNotificationHub, didReceivePushNotification message: MSNotificationHubMessage) {
print("notificationHub...")
let title = message.title ?? ""
let body = message.body ?? ""
print("title: \(title)")
print("body: \(body)")
let userInfo = ["message": message]
NotificationCenter.default.post(name: NSNotification.Name("MessageReceived"), object: nil, userInfo: userInfo)
guard let aps = message.userInfo["aps"] as? [String: AnyObject] else {
return
}
...
}
Second, this post provided the answer which I adapted for my project:
final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, MSNotificationHubDelegate {
var navMgr: NavigationManager = NavigationManager()
...
}
and
#UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(sessionInfo)
.environmentObject(appDelegate.navMgr)
.onOpenURL { url in
print("onOpenURL: \(url)")
if sessionInfo.processDeepLink(url: url) {
print("deep link TRUE")
} else {
print("deep link FALSE")
}
}
}
}
I've successfully set my first view controller to STPAddCardViewController. I now need to get the user information in the STPPaymentCardTextField. Problem is, I'm used to using the storyboard to make outlets. How do I detect the STPPaymentCardTextField programmatically?
I've tried:
class ViewController: STPAddCardViewController, STPPaymentCardTextFieldDelegate {
let paymentCardTextField = STPPaymentCardTextField()
func paymentCardTextFieldDidChange(_ textField: STPPaymentCardTextField) {
print(paymentCardTextField.cardNumber)
//ERROR: printing nil in the console
}
}
But I'm getting nil as an output. Any help?
You should use either STPAddCardViewController, or STPPaymentCardTextField, not both. The SDK's ViewControllers are not designed to be extended. The intended use is:
class MyVC : STPAddCardViewControllerDelegate {
override func viewDidLoad() {
…
let addCardView = STPAddCardViewController()
addCardView.delegate = self
// Start the addCardView
self.navigationController.pushViewController(addCardView, animated: true)
}
…
func addCardViewController(_ addCardViewController: STPAddCardViewController, didCreatePaymentMethod paymentMethod: STPPaymentMethod, completion: #escaping STPErrorBlock) {
// TODO: do something with paymentMethod
// Always call completion() to dismiss the view
completion()
}
func addCardViewControllerDidCancel(_ addCardViewController: STPAddCardViewController) {
// TODO: handle cancel
}
}
But rather than my partial example I'd recommend reading these docs and trying out this example iOS code. Best wishes!
I'm new to swift development and building a simple app for the apple watch. I'd like a short sound to play when a button is tapped. Is this possible?
From what I understand the best way to do this is with AVFoundation and AVAudioPlayer. There seem to have been a lot of updates for this in the last few releases and I'm finding conflicting advice. Based on a few tutorials I've put this simple test together, but I'm getting a "Thread 1:Fatal error: Unexpectedly found nil when unwrapping an Optional value" Here is my code:
import WatchKit
import Foundation
import AVFoundation
var dogSound = AVAudioPlayer()
class InterfaceController: WKInterfaceController {
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
#IBAction func playTapped() {
let path = Bundle.main.path(forResource: "Dog", ofType: ".mp3")!
let url = URL(fileURLWithPath: path)
do {
dogSound = try AVAudioPlayer(contentsOf: url)
dogSound.play()
} catch {
//couldn't load file :(
}
}
}
My audio file is an mp3 named Dog in the Assets.xcassets folder in WatchKit Extension and I have added AVFoundation.framework to the Link Binary with Libraries
What am I doing wrong and is there a tutorial for the right way to implement this? Thanks a lot!
The question is similar to this one:
Display a view or splash screen before applicationDidEnterBackground (to avoid active view screenshot)
The link above also shows code examples how to implement it.
I need to show for example white screen when the app enters background on "multitasking screen":
The problem is that way doesn't work on iOS 13! How to fix this issue?
You show that in this UIWindowSceneDelegate method. Implement whatever logic you want in it.
func sceneDidEnterBackground(_ scene: UIScene)
I use this (my own) implementation for functionality you ask for both iOS 12 & iOS 13 support
AppDelegate:
private var blankWindow: BlankWindow?
// MARK: Shared AppDelegate
extension AppDelegate {
static func blankWindowShouldAppear(blankWindow: inout BlankWindow?) {
blankWindow = BlankWindow(frame: UIScreen.main.bounds)
blankWindow?.makeKeyAndVisible()
}
static func blankWindowShouldDisappear(window: UIWindow?, blankWindow: inout BlankWindow?) {
window?.makeKeyAndVisible()
blankWindow = nil
}
#available(iOS 13.0, *)
static func blankWindowShouldAppear(_ windowScene: UIWindowScene, blankWindow: inout BlankWindow?) {
blankWindow = BlankWindow(windowScene: windowScene)
blankWindow?.makeKeyAndVisible()
}
}
// MARK: Old life cycle methods
extension AppDelegate {
/// ⚠️ Methods here will not be called under iOS 13 due to new SceneDelegate life cycle
func applicationWillEnterBackground(_ application: UIApplication) {
AppDelegate.blankWindowShouldAppear(blankWindow: &blankWindow)
}
func applicationWillEnterForeground(_ application: UIApplication) {
AppDelegate.blankWindowShouldDisappear(window: window, blankWindow: &blankWindow)
}
}
SceneDelegate:
private var blankWindow: BlankWindow?
// MARK: New life cycle methods
#available(iOS 13.0, *)
extension SceneDelegate {
/// ⚠️ As for now, we use fallback to AppDelegate shared methods to reduce code duplication
/// Not all of the new life cycle methods are implemented here, yet
func sceneWillEnterForeground(_ scene: UIScene) {
AppDelegate.blankWindowShouldDisappear(window: window, blankWindow: &blankWindow)
}
func sceneWillEnterBackground(_ scene: UIScene) {
guard let windowScene = (scene as? UIWindowScene) else { return }
AppDelegate.blankWindowShouldAppear(windowScene, blankWindow: &blankWindow)
}
}
Where BlankWindow class is a UIWindow you wanna show to a user at this moment
I'm a beginner on TVOS.
I'd like to create an hybrid app on AppleTV using a native app and TVMLKIT.
My native application is just a simple native app with buttons (using swift).
When we click on a button, I launch a a javascript app using TVLMKIT and TVJS.
My TVJS as uses the Player to display a video.
When the video is over, I want to close the TVJS app and back to the native ViewController.
My problem is that when I back to native app, I loose the focus on my native View (the app is frozen).
native ViewController:
import UIKit
import TVMLKit
class ViewController: UIViewController, TVApplicationControllerDelegate {
var window: UIWindow?
var appController: TVApplicationController?
var appControllerContext = TVApplicationControllerContext();
static let TVBaseURL = "http://localhost:9001/"
static let TVBootURL = "\(ViewController.TVBaseURL)/client/js/application.js"
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBOutlet weak var label: UILabel!
#IBOutlet weak var viewAd: UIView!
#IBAction func clickOnlaunchAd(sender: AnyObject) {
window = UIWindow(frame: UIScreen.mainScreen().bounds)
guard let javaScriptURL = NSURL(string: ViewController.TVBootURL) else {
fatalError("unable to create NSURL")
}
appControllerContext.javaScriptApplicationURL = javaScriptURL
appControllerContext.launchOptions["BASEURL"] = ViewController.TVBaseURL
appController = TVApplicationController(context: appControllerContext, window: window,delegate: self)
}
#IBAction func clickOnChangeText(sender: AnyObject) {
label.text = "changed";
}
func appController(appController: TVApplicationController, didStopWithOptions options: [String : AnyObject]?) {
self.setNeedsFocusUpdate()
self.updateFocusIfNeeded()
}
func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext){
let notifyEventToNative : #convention(block) (NSString!) -> Void = {
(string : NSString!) -> Void in
print("[log]: \(string)\n")
self.appController?.stop()
}
jsContext.setObject(unsafeBitCast(notifyEventToNative, AnyObject.self), forKeyedSubscript: "notifyEventToNative")
}
}
Just before calling "notifyEventToNative" from my TVJS, I call "navigationDocument.clear();" to clear the TVML view.
I can see my native app but I can't interact with it.
Any ideas?
Thanks.
I also had the same problem. I was opened a TVML document from the UIViewController. And I also lost the focus. So, first of all I can advice you to override var called preferredFocusedView in your ViewController. In this method you can return reference to viewAd. But the better solution would be to wrap your ViewController into the TVML-item (with the TVMLKit framework). In that case I hope that you will have no problems with focus because you will use TVML during the whole application.