Can I use the Snapchat SDK (SnapKit) with SwiftUI?

2.4k views Asked by At

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
3

There are 3 answers

2
bze12 On BEST ANSWER

@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
    }
3
Stephen.Alger On

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]

0
JAHelia On

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