はじめに
【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のドキュメントは丁寧に説明されているので本記事内での説明は不要かもしれませんが、実装までの一連の流れをイメージできるようにしたいため記載します。
※参考:プロジェクトを設定する | LINE Developers
LINE Developersでログインしコンソールを選択
プロバイダーを作成
チャネル設定でLINEログインを選択しチャネルを作成
ターミナルでCocoaPodsを導入
$ pod init
Podfileに以下を追加
Podfile
pod 'LineSDKSwift'
ターミナルでLINE SDKをインストール
$ pod install
プロジェクト設定のGeneralでbundle IDを確認
LINE Developersのトップ > プロバイダー > チャネル > LINEログイン設定と遷移し、bundle IDを入力
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のトップ > プロバイダー > チャネル > チャネル基本設定に記載されています。
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"
ネストされたクラスである必要はないものの、ネストさせるとカプセル化できること(意図しない場所で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 Apple(Appleでサインイン)の実装について記事にします。
参考
・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