본문 바로가기

Swift

Facebook 로그인 구현 + Firebase 연결

https://explorer89.tistory.com/310

 

페이스북 로그인 기능 구현

https://developers.facebook.com/apps/?show_reminder=true Facebook business.facebook.comhttps://youtu.be/W8NzdN0h50I?si=fwvW92WfBDj__GZZ https://zoeful-log.tistory.com/131 [Swift] SwiftUI로 Facebook Login 구현하기Facebook 공식문서를 보고 정

explorer89.tistory.com

 

이전에 작성한 게시글 입니다. 

 

이번에는 Facebook + Firebase 로그인 구현을 Combine을 통해 진행했습니다. 

 

여기서 Appdelegate.swift 내용도 반드시 적어야합니다. 

 

Appdelegate.swift

import UIKit
import CoreData
import FBSDKCoreKit
import FirebaseCore

@main
class AppDelegate: UIResponder, UIApplicationDelegate {



    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         // Firebase 초기화
         FirebaseApp.configure()

         // Facebook SDK 초기화
         ApplicationDelegate.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
         
         return true
     }

     func application(
         _ app: UIApplication,
         open url: URL,
         options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
         // Facebook URL 처리
         return ApplicationDelegate.shared.application(app, open: url, options: options)
     }
     ...

 

 

Scenedelegate.swift

import UIKit
import FBSDKCoreKit
import FBSDKLoginKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    // Facebook 로그인
    // 이 코드는 앱이 시작될 때 SDK를 초기화하며, 로그인이나 공유 작업을 수행할 때
    // SDK가 Facebook 네이티브 앱의 로그인과 공유를 처리하도록 합니다.
    // 그렇지 않으면 사용자가 Facebook에 로그인한 상태에서만 앱 내 브라우저를 통해 로그인할 수 있습니다.
    func scene(_ scene: UIScene,
               openURLContexts URLContexts: Set<UIOpenURLContext>) {
        guard let url = URLContexts.first?.url else {
            return
        }

        ApplicationDelegate.shared.application(
            UIApplication.shared,
            open: url,
            sourceApplication: nil,
            annotation: [UIApplication.OpenURLOptionsKey.annotation]
        )
    }
    ...

 

 

AuthenticationManager.swift

import Foundation
import FirebaseAuth
import Combine

final class AuthenticationManager {
    
    
    static let shared = AuthenticationManager()
    
    private init() {}
    
    /// Facebook 등의 소셜 로그인 처리
    func signIn(with credential: AuthCredential) -> AnyPublisher<User, Error> {
        
        return Future { promise in
            Auth.auth().signIn(with: credential) { authResult, error in
                if let error = error {
                    promise(.failure(error))  // 에러 처리
                } else  if let user = authResult?.user {
                    promise(.success(user))   // 성공
                }
            }
        }
        .eraseToAnyPublisher()
    }
}

 

 

AutheticationViewModel.swift

import Foundation
import FirebaseAuth
import Combine

final class AuthenticationViewModel: ObservableObject {
    
    @Published var user: User?
    @Published var error: String?
    
    private var subscription: Set<AnyCancellable> = []
    
    
    func createUser(with credential: AuthCredential) {
        
        // 에러 초기화 목적
        error = nil
        
        AuthenticationManager.shared.signIn(with: credential)
            .sink { [weak self] completion in
                switch completion {
                case .failure(let error):
                    self?.error = error.localizedDescription
                case .finished:
                    print("회원 가입 성공")
                } 
            } receiveValue: { [weak self] user in
                self?.user = user
            }
            .store(in: &subscription)
    }
    
}

 

 

OnboardingViewController.swift

- extension을 통해 페이스북 로그인 메서드 구현 

- bindView()를 통해 뷰모델의 상태 변화(user, error)를 UI에 반영하기 위해 구현

import UIKit
import FBSDKLoginKit
import Combine
import FirebaseAuth

class OnboardingViewController: UIViewController {
    
    // MARK: - ViewModel
    private let viewModel = AuthenticationViewModel()
    private var cancellables: Set<AnyCancellable> = []
    
    
    // MARK: - UI Components
    private let onboardingView: OnboardingView = {
        let view = OnboardingView()
        return view
    }()
    
    
    // MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemGreen
        
        configureConstraints()
        
        onboardingView.calledFaecbookLoginButton().delegate = self
        
        if let token = AccessToken.current,
            !token.isExpired {
            dismiss(animated: true)
        }
        bindView()
    }
    
    
    // MARK: - Layouts
    private func configureConstraints() {
        
        view.addSubview(onboardingView)
        
        onboardingView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            
            onboardingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            onboardingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            onboardingView.topAnchor.constraint(equalTo: view.topAnchor),
            onboardingView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
            
        ])
        
    }
    
    // MARK: - Functions
    // ViewModel을 연동하는 함수
    private func bindView() {
        
        viewModel.$user.sink { [weak self] user in
            guard user != nil else { return }
            print("로그인 성공: \(user?.displayName ?? "사용자 이름 없음")")
            self?.dismiss(animated: true)    // 로그인 성공 후 화면 닫기
        }
        .store(in: &cancellables)
        
        viewModel.$error.sink { [weak self] error in
            guard let error = error else { return }
            print("로그인 실패: \(error)")
            self?.showErrorAlert(message: error)
        }
        .store(in: &cancellables)

    }
    
    // 에러를 나타내는 경고창함수
    private func showErrorAlert(message: String) {
        let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "Ok", style: .default))
        present(alert, animated: true)
    }
    
}

// MARK: - Extensions

/// facebook 로그인 구현
extension OnboardingViewController: LoginButtonDelegate {
    
    func loginButton(_ loginButton: FBSDKLoginKit.FBLoginButton, didCompleteWith result: FBSDKLoginKit.LoginManagerLoginResult?, error: (any Error)?) {
        if let error = error {
            print("Facebook 로그인 실패: \(error.localizedDescription)")
            return
        }
        
        // Facebook 로그인 성공 시 Firebase 인증 처리
        guard let accessToken = AccessToken.current else { return }
        let credential = FacebookAuthProvider.credential(withAccessToken: accessToken.tokenString)
        
        viewModel.createUser(with: credential)
        
    }
    
    func loginButtonDidLogOut(_ loginButton: FBSDKLoginKit.FBLoginButton) {
        print("Facebook 로그아웃 성공")
    }
    
}

 

 

왜 콜백 함수와 Combine을 쓸 때 Future - Promise를 써야 하는가?

func signIn(with credential: AuthCredential) -> AnyPublisher<User, Error> {

    return Future { promise in
        Auth.auth().signIn(with: credential) { authResult, error in
            if let error = error {
                promise(.failure(error))  // 에러 처리
            } else  if let user = authResult?.user {
                promise(.success(user))   // 성공
            }
        }
    }
    .eraseToAnyPublisher()
}

 

 

Combine은 비동기 작업을 처리하고 결과를 스트림으로 관리할 수 있는 강력한 도구입니다. 하지만 기존의 비동기 작업 방식인 콜백 기반 코드를 Combine으로 변환하려면, 콜백을 Publisher로 감싸야 합니다. 이때, Future가 적합합니다.

  • 콜백의 문제점:
    • 콜백은 단발성입니다. 비동기 작업의 성공/실패를 처리하지만, 이를 Combine의 Publisher 스트림으로 쉽게 통합하기 어렵습니다.
    • 코드의 가독성이 떨어지고 관리가 어렵습니다.
  • Future의 역할:
    • Future는 단일 비동기 작업의 결과를 Publisher로 변환합니다.
    • 작업이 완료되면 Promise를 통해 성공(.success) 또는 실패(.failure) 상태를 전달할 수 있습니다.
    • Combine 기반의 작업 체인에 자연스럽게 통합할 수 있습니다.

 

Future - Promise 코드 설명

  1. Future 정의:
    • Future는 하나의 결과를 비동기적으로 반환하는 Combine의 Publisher입니다.
    • Future는 Promise를 통해 작업 완료 시 결과를 전달받습니다.
  2. Promise 사용:
    • Promise는 Future의 결과를 설정하는 클로저입니다.
    • 성공 상태를 전달하려면 .success(value)를, 실패 상태를 전달하려면 .failure(error)를 호출합니다.
  3. 결과 구독 및 사용:
    • Future를 반환하는 메서드는 Combine에서 체이닝하거나 구독할 수 있습니다.

'Swift' 카테고리의 다른 글

ViewModel  (1) 2025.01.08
setCustomSpacing(_:after:)의 역할  (0) 2025.01.05
UICollectionViewCompositionalLayout 관련 데이터 소스 관리  (0) 2025.01.03
async / await 사용해보기  (0) 2025.01.02
async/await란?  (0) 2024.12.18