Swift・iOS

Swiftを中心に学んだことを記録に残すブログです。技術に関係ない記事もたまに書いています。

【SwiftUI】LINEログインの実装

 

はじめに

【SwiftUI】IDとパスワードでログインする画面を実装する - Swift・iOSの続きです。今回はLINEログインを実装していきます。

 

※ログイン機能については以下の記事でも取り上げています。

【SwiftUI】Sign In with Appleの実装 - Swift・iOS

【SwiftUI】ログイン画面の実装まとめ - Swift・iOS

 

開発環境

Xcode 12.0.1

・Swift 5.3

・CocoaPods 1.10.0

・LINE SDK for iOS Swift 5.7.0

 

実装

設定手順

以下のページを参考に設定を進めます。LINEのドキュメントは丁寧に説明されているので本記事内での説明は不要かもしれませんが、実装までの一連の流れをイメージできるようにしたいため記載します。

※参考:プロジェクトを設定する | LINE Developers

 

LINE Developersでログインしコンソールを選択

f:id:hfoasi8fje3:20201030165514p:plain

 

プロバイダーを作成

f:id:hfoasi8fje3:20201030165945p:plain

 

チャネル設定でLINEログインを選択しチャネルを作成

f:id:hfoasi8fje3:20201030170247p:plain

  

ターミナルでCocoaPodsを導入

$ pod init

 

Podfileに以下を追加

Podfile

pod 'LineSDKSwift'

 

ターミナルでLINE SDKをインストール

$ pod install

 

プロジェクト設定のGeneralでbundle IDを確認

f:id:hfoasi8fje3:20201027174612p:plain

 

LINE Developersのトップ > プロバイダー > チャネル > LINEログイン設定と遷移し、bundle IDを入力

f:id:hfoasi8fje3:20201027173709p:plain

 

Info.plistファイルを設定

Info.plistを右クリック→Open Asを選択→Source Codeを選択し、以下を</dict>タグの直前に挿入

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <!-- LINEからアプリに戻る際に利用するURLスキーマを追加 -->
            <string>line3rdp.$(PRODUCT_BUNDLE_IDENTIFIER)</string>
        </array>
    </dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
    <!-- アプリからLINEを起動する際に利用するURLスキーマを追加 -->
    <string>lineauth2</string>
</array>

 

コード

AppDelegate.swift

application(_:didFinishLaunchingWithOptions:)に以下処理を追加

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // LINE SDKを設定
        LoginManager.shared.setup(channelID: "{チャネルID}", universalLinkURL: nil)
        
        return true
    }

 ※チャネルIDはLINE Developersのトップ > プロバイダー > チャネル > チャネル基本設定に記載されています。

f:id:hfoasi8fje3:20201028210618p:plain

 

LoginView.swift

bodyにTextとLineLoginButtonを追加

 var body: some View {
        VStack(spacing: 15) {            
            // ※アカウントIDでのログインに関する実装は省略
            
            Text("LINEログイン")
                .bold()
            
            LineLoginButton()
                .frame(width: 200, height: 45)
        }
    }

 

LineLoginButton.swift

import SwiftUI
import LineSDK

struct LineLoginButton: UIViewRepresentable {
    class Coordinator: NSObject, LoginButtonDelegate {
        var parent: LineLoginButton
        
        init(_ parent: LineLoginButton) {
            self.parent = parent
        }
        
        func loginButton(_ button: LoginButton, didSucceedLogin loginResult: LoginResult) {
            print("LINE認証成功")
            print("アクセストークン:\(loginResult.accessToken.value)")
            print("ここでログイン処理を呼び出す")
        }
        
        func loginButton(_ button: LoginButton, didFailLogin error: LineSDKError) {
            print("LINE認証エラー: \(error)")
        }
        
        func loginButtonDidStartLogin(_ button: LoginButton) {
            print("LINE認証開始")
        }
    }

    // Coordinatorを作成するメソッド
    // CoordinatorはUIViewRepresentableに準拠した構造体LineLoginButtonの状況(Context)の一部を
    // 他のSwiftUIのインターフェースに伝達できるようにする
    // そのため、作成したCoordinatorを使用して、デリゲート、データソース、ユーザーイベントへの応答などを実装できる
    // makeCoordinatorで生成したCoordinatorのインスタンスにはupdateUIViewの引数contextからアクセスできる
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    // Viewを初期化
    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        return view
    }
    
    // Viewの状態を更新
    func updateUIView(_ view: UIView, context: Context) {
        let loginButton = LoginButton()
        // makeCoordinatorで生成したCoordinatorクラスのインスタンスを指定
        loginButton.delegate = context.coordinator
        
        // 認証時にユーザーへプロフィール情報の取得について許可をとる
        loginButton.permissions = [.profile]
        // nilを指定すると、現在のViewController階層の最上位のViewControllerが使用される
        loginButton.presentingViewController = nil
        
        // makeUIViewで生成したViewにLINE SDKで用意されたLoginButtonを追加
        view.addSubview(loginButton)
        // ログインボタンのレイアウト設定
        loginButton.translatesAutoresizingMaskIntoConstraints = false
        loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        loginButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }
}

struct LineLoginButton_Previews: PreviewProvider {
    static var previews: some View {
        LineLoginButton()
    }
}

 

 

 

補足:構造体にCoordinatorクラスが内包されている理由

 "It doesn’t need to be a nested class, although it’s a good idea because it neatly encapsulates the functionality"

引用: Using coordinators to manage SwiftUI view controllers - a free Hacking with iOS: SwiftUI Edition tutorial

ネストされたクラスである必要はないものの、ネストさせるとカプセル化できること(意図しない場所でCoordinatorクラスがインスタンス化されることを事前に避けることができるため)が良いという記載を参考にしたためです。

AppleのSwiftUIのチュートリアルでも、構造体にネストする形でCoordinatorに指定するクラスを定義しています。

※参考:https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit

 

課題:LineLoginButton.swift内の実装がMVVMに沿ったものではない問題

LINEログインのドキュメントにしたがって実装する中で、「構造体LineLoginButtonの実装はViewに書く?ViewModelに書く?」という点に悩みました。

構造体LineLoginButtonは、以下の特徴があります。

・UIViewRepresentableに準拠した構造体

・Coordinatorを生成するメソッド、Viewを初期化、更新するメソッドを持つ

・LoginButtonDelegateに準拠したクラスを持つ

 

UIViewRepresentableはViewを継承しているのでLoginView.swiftに実装するのがよいのでしょうか?

※参考:https://developer.apple.com/documentation/swiftui/uiviewrepresentable

それとも、UIKitのViewとSwiftUIのViewを相互作用させるためのCoordinatorを生成する「メソッド」を持つので、LoginViewModel.swiftに実装でしょうか?

LoginButtonDelegateに準拠したクラスはデリゲートメソッドを持つのでLoginViewModel.swiftに実装でしょうか?

 

実装案としては以下を考えました。

・LoginButtonDelegateに準拠したクラスを構造体LineLoginButtonから分離させ、構造体はViewと判断してLoginView.swiftに実装、クラスはViewModelと判断してLoginViewModel.swiftに実装

 →Coordinatorを生成するメソッドは構造体内に実装しないといけないため、LoginViewModel.swiftに実装することは諦める。。

・構造体LineLoginButtonはCoordinatorを生成する「メソッド」、Viewを初期化、更新する「メソッド」を持つためViewModelと判断、LoginButtonDelegateに準拠したクラスもViewModelと判断してどちらもLoginViewModel.swiftに実装

 →「UIViewRepresentableはViewを継承している=構造体LineLoginButtonはViewである」には目をつぶる・・・MVVMじゃない。。

・「UIViewRepresentableはViewを継承している=構造体LineLoginButtonはViewである」であり、「Viewを初期化、更新する中でアクセスするCoordinatorもViewを構成するために必要な要素」と解釈して、LoginView.swiftに実装

 →「Viewを初期化、更新する中でアクセスするCoordinatorもViewを構成するために必要な要素」って違和感ありすぎるのでは・・・?とりあえずMVVMではない。。

・無理にLoginView.swift、LoginViewModel.swiftに分けずにLineLoginButton.swiftファイルを追加してそこにまとめて実装(上記のコード)

 →MVVMじゃない。。

 

どれもいまいちですね。。

この時点で相当悩んだのですが、よくよく考えてみると、デリゲートを使う時点で参照が発生するのでMVVMではないのでは?と思いました。

「デリゲートは、オブジェクトインスタンスへの参照とメソッドへの参照をペアにしてカプセル化するものである。」

※引用:デリゲート (プログラミング) - Wikipedia

 

そうなると、ViewとViewModel間でデータバインディングが成り立つように修正しないといけないのですが、LINE SDKのドキュメントでデリゲートを使って実装している以上、デリゲートを使わなくても不具合なく動くようにSDKの実装を確認しながら修正していく必要があり、また、将来的にSDKのバージョンアップによって意図しない不具合や動かなくなるのは避けたいので、今回はMVVMに沿っていないまま実装しました・・・。。

 

おわりに

次回はSign in with AppleAppleでサインイン)の実装について記事にします。

 

参考

LINE SDK for iOS Swiftの概要 | LINE Developers

プロジェクトを設定する | LINE Developers

https://developer.apple.com/documentation/swiftui/uiviewrepresentable

https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable

https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit

Using coordinators to manage SwiftUI view controllers - a free Hacking with iOS: SwiftUI Edition tutorial

デリゲート (プログラミング) - Wikipedia