Swift・iOS

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

【SwiftUI】LINEログインとSign in with Appleを含むログイン画面の実装

【スポンサーリンク】

 

開発環境

Xcode 12.0.1

・Swift 5.3

・CocoaPods 1.10.0

・LINE SDK for iOS Swift 5.7.0

 

実装イメージ

・IDとパスワードでのログイン、LINEログイン、Sign in with Appleを表示

・IDとパスワードでのログインはそれぞれの入力文字が半角英数でないとログインボタンが有効にならない

・LINEログインの認証が成功した場合はLINEのアクセストークンをデバッグエリアに出力する

Sign In with Appleの認証が成功した場合はデバッグエリアにユーザー識別子、フルネーム、メールアドレス、認証コードを出力する

APIへのリクエスト処理は省略します。

f:id:hfoasi8fje3:20201031184702p:plain

  

各ログインの実装

ID・パスワードでのログイン

実装方針

MVVMに沿った実装にしたいため、以下のようにView、ViewModel、Modelへ処理を分けたいと思います。

※参考:PEAKS(ピークス)|iOSアプリ設計パターン入門

View

ユーザー操作の受付と画面表示を担当

・IDとパスワードを入力するTextFieldを表示

・IDとパスワードに入力された文字をViewModelに通知

・ログインボタンを表示

・ログインボタンが選択されたらViewModelに通知

 

ViewModel

Viewに表示するためのデータを保持したり、Viewからイベントを受け取ってModelの処理を呼び出したりデータを更新する

・IDとパスワードそれぞれに入力された文字を保持する変数

・IDとパスワードが半角英数で入力されている状態か保持する変数

・ログインボタンの選択状態を保持する変数

・ログインボタンが選択されたらModelからログイン処理を呼び出す

 

Model

UIに関係しないロジックを持つ

・ログイン処理(本記事では省略)

 

その他

・半角英数かどうか判別するStringの拡張

 

コード

LoginView.swift

import SwiftUI

struct LoginView: View {
    @ObservedObject var viewModel: LoginViewModel
    
    init(viewModel: LoginViewModel) {
        self.viewModel = viewModel
    }
    
    var body: some View {
        VStack(spacing: 15) {
            Text("アカウントIDでログイン")
                .bold()
            
            // 入力した文字をLoginViewModelの変数idに通知
            TextField("ID(半角英数)", text: $viewModel.id)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(width: 300)
            
            // 入力した文字をLoginViewModelの変数passwordに通知
            TextField("パスワード(半角英数)", text: $viewModel.password)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(width: 300)
            
            Button(action: {
                // ボタンが選択されたことをLoginViewModelの変数isLoginButtonTappedに通知
                viewModel.isLoginButtonTapped = true
            }) {
                Text("ログイン")
            }
            .frame(width: 200, height: 45)
            .foregroundColor(Color.white)
            // ボタンの有効/無効状態に合わせて背景の色を変更
            .background(viewModel.isValidId && viewModel.isValidPassword ? Color.blue : Color.gray)
            .cornerRadius(10, antialiased: true)
            // IDとパスワードがどちらも半角英数で入力されていればボタンを有効にする
            .disabled(!viewModel.isValidId || !viewModel.isValidPassword)
        }
    }
}

struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginView(viewModel: LoginViewModel())
    }
}

 

LoginViewModel.swift

import Combine

class LoginViewModel: ObservableObject {
    @Published var id: String = ""
    @Published var password: String = ""
    
    // TextFieldに入力した文字が半角英数かどうか判別する変数
    @Published var isValidId: Bool = false
    @Published var isValidPassword: Bool = false
    
    @Published var isLoginButtonTapped: Bool = false
    
    private var disposables = [AnyCancellable]()
    
    init() {
        $id
            .sink(receiveValue: {
                self.isValidId = $0.isAlphanumeric && !$0.isEmpty ? true : false
            })
            .store(in: &disposables)
        
        $password
            .sink(receiveValue: {
                self.isValidPassword = $0.isAlphanumeric && !$0.isEmpty ? true : false
            })
            .store(in: &disposables)
        
        $isLoginButtonTapped
            .sink(receiveValue: { isTapped in
                if isTapped == true {
                    print("ここでログイン処理を呼び出す")
                }
            })
            .store(in: &disposables)
    }
}

 

String+Extensions.swift

extension String {
    // 半角英数かどうか判別
    var isAlphanumeric: Bool {
        return !isEmpty && range(of: "[^a-zA-Z0-9]", options: .regularExpression) == nil
    }
}

 

SceneDelegate.swift

import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // 起動後の画面をログイン画面に設定
        let viewModel = LoginViewModel()
        let loginView = LoginView(viewModel: viewModel)

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: loginView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}

 

LINEログイン

設定手順

以下のページを参考に設定を進めます。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 Apple

Apple Developer Program - Apple Developerへの登録が必要です。

設定手順

Apple DeveloperのAccountからログイン

f:id:hfoasi8fje3:20201030191448p:plain

 

Certificates, Identifiers & ProfilesのIdentifiersを追加します

※Certificatesの生成手順と、Deviceの登録手順に関しては省略します。

f:id:hfoasi8fje3:20201030202207p:plain

 

App IDsを選択してContinueを選択

f:id:hfoasi8fje3:20201030202339p:plain

 

DescriptionとBundle IDを入力し、Sign In with Appleにチェックを入れてContinueを選択

f:id:hfoasi8fje3:20201030202517p:plain

 

秘密鍵を作成するためにKeysの+かCreate a keyを選択

f:id:hfoasi8fje3:20201030204740p:plain

 

Sign in with Appleにチェックを入れて、Configureを選択

f:id:hfoasi8fje3:20201030205256p:plain

 

Primary App IDを選択してSaveを選択

f:id:hfoasi8fje3:20201030205504p:plain

 

Key Nameを入力してContinueを選択

f:id:hfoasi8fje3:20201030205951p:plain

 

Registerを選択

f:id:hfoasi8fje3:20201030210509p:plain

 

Downloadを選択

※注意:Keyをダウンロードした後に再ダウンロードすることはできません。そのため、ダウンロードしたKeyは必ず安全な場所に保存してください。

f:id:hfoasi8fje3:20201030210909p:plain

 

Provisioning Profileを生成するため、Profilesに移動して+を選択

f:id:hfoasi8fje3:20201030212158p:plain

 

今回は動作テスト用を想定しているので、DevelopmentのiOS Developmentを選択した状態で、Continueを選択

f:id:hfoasi8fje3:20201030212530p:plain

 

App IDを選択し、Continueを選択

f:id:hfoasi8fje3:20201030212902p:plain

 

Certificatesを選択し、Continueを選択

f:id:hfoasi8fje3:20201030213122p:plain

 

Deviceを選択し、Continueを選択

f:id:hfoasi8fje3:20201030213345p:plain

 

Provisioning Profileの名前を入力してGenerateを選択

f:id:hfoasi8fje3:20201030213652p:plain

 

Downloadを選択してダウンロード

ダウンロード完了後、ファイルをダブルクリックして取り込む

f:id:hfoasi8fje3:20201030214030p:plain

 

取り込んだProvisioning Profileを設定

(右にある"!"を選択するとSign In with Appleが有効になっていることが確認できる)

f:id:hfoasi8fje3:20201030221256p:plain

 

Provisioning Profileを設定した画面にある+Capabilityを選択し、Sign In with Appleを選択

f:id:hfoasi8fje3:20201030222303p:plain

 

Sign In with Appleが追加される

f:id:hfoasi8fje3:20201030222916p:plain

 

コード

 LoginView.swift

AuthenticationServicesをimport

import AuthenticationServices

body内にTextとSignInWithAppleButtonを追加

      Text("Sign In with Apple")
                .bold()
            
            SignInWithAppleButton(.signIn) { request in
                request.requestedScopes = [.fullName, .email]
            } onCompletion: { authResults in
                viewModel.appleAuthResults = authResults
            }
            .signInWithAppleButtonStyle(.black)
            .frame(width: 200, height: 45)

 

LoginViewModel.swift

AuthenticationServicesをimport

import AuthenticationServices

変数を追加

@Published var appleAuthResults: Result<ASAuthorization, Error>?

initに以下を追加

 $appleAuthResults
            .sink(receiveValue: { results in
                switch results {
                case .success(let authResults):
                    switch authResults.credential {
                    case let appleIDCredential as ASAuthorizationAppleIDCredential:
                        print("userIdentifier:\(appleIDCredential.user)")
                        print("fullName:\(String(describing: appleIDCredential.fullName))")
                        print("email:\(String(describing: appleIDCredential.email))")
                        print("authorizationCode:\(String(describing: appleIDCredential.authorizationCode))")
                        
                        print("ここでログイン処理を呼び出す")
                        
                    default:
                        break
                    }
                    
                case .failure(let error):
                    print(error.localizedDescription)
                    
                default:
                    break
                }
            })
            .store(in: &disposables)

 

メールアドレスとフルネームの取得条件である「初回」の定義

メールアドレスとフルネームは初回のみ取得できます。

実機で動かしてみたところ、以下の動作が確認できました。

・ユーザーがアプリをインストールして初めてSign In with Appleを実行した時はメールアドレスとフルネームを取得できる

Sign In with Appleを実行後、ユーザーが設定 > パスワードとセキュリティ > Apple IDを使用中のApp > アプリを選択 > Apple IDの使用を停止する、を選択し、再度Sign In with Appleを実行した時はメールアドレスとフルネームを取得できる

Sign In with Appleを実行後、ユーザーがアプリを削除し再ダウンロードしてSign In with Appleを実行してもメールアドレスとフルネームは取得できない(=初回扱いではない)

 

【スポンサーリンク】
 

 

上記の事象から、「初回」の定義は「該当アプリでSign In with Appleで認証をしたことがない状態でSign In with Appleを実行した時」、もしくは、「Sign In with Appleで認証後、該当アプリでのApple IDの使用を停止して再度Sign In with Appleを実行した時」となります。そのため、「初回」以降は該当のアプリの削除や再インストールの操作に関係なく、該当のアプリでApple IDの使用を停止するまで、「初回」状態に戻ることはありません。

 

全体のコード

AppDelegate.swift

import UIKit
import LineSDK

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

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

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }
}

 

SceneDelegate.swift

import UIKit
import SwiftUI
import LineSDK

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // 起動後の画面をログイン画面に設定
        let viewModel = LoginViewModel()
        let loginView = LoginView(viewModel: viewModel)

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: loginView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }
    
    // URLでアプリが開かれた場合に実行される
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        _ = LoginManager.shared.application(.shared, open: URLContexts.first?.url)
    }
}

 

LoginView.swift

import SwiftUI
import AuthenticationServices

struct LoginView: View {
    @ObservedObject var viewModel: LoginViewModel
    
    init(viewModel: LoginViewModel) {
        self.viewModel = viewModel
    }
    
    var body: some View {
        VStack(spacing: 15) {
            Text("アカウントIDでログイン")
                .bold()
            
            // 入力した文字をLoginViewModelの変数idに通知
            TextField("ID(半角英数)", text: $viewModel.id)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(width: 300)
            
            // 入力した文字をLoginViewModelの変数passwordに通知
            TextField("パスワード(半角英数)", text: $viewModel.password)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(width: 300)
            
            Button(action: {
                // ボタンが選択されたことをLoginViewModelの変数isLoginButtonTappedに通知
                viewModel.isLoginButtonTapped = true
            }) {
                Text("ログイン")
            }
            .frame(width: 200, height: 45)
            .foregroundColor(Color.white)
            // ボタンの有効/無効状態に合わせて背景の色を変更
            .background(viewModel.isValidId && viewModel.isValidPassword ? Color.blue : Color.gray)
            .cornerRadius(10, antialiased: true)
            // IDとパスワードがどちらも半角英数で入力されていればボタンを有効にする
            .disabled(!viewModel.isValidId || !viewModel.isValidPassword)
            .padding(EdgeInsets(top: 0, leading: 0, bottom: 30, trailing: 0))
            
            Text("LINEログイン")
                .bold()
            
            LineLoginButton()
                .frame(width: 200, height: 45)
                .padding(EdgeInsets(top: 0, leading: 0, bottom: 30, trailing: 0))
            
            Text("Sign In with Apple")
                .bold()
            
            SignInWithAppleButton(.signIn) { request in
                request.requestedScopes = [.fullName, .email]
            } onCompletion: { authResults in
                viewModel.appleAuthResults = authResults
            }
            .signInWithAppleButtonStyle(.black)
            .frame(width: 200, height: 45)
        }
    }
}

struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginView(viewModel: LoginViewModel())
    }
}

 

LoginViewModel.swift

import Combine
import AuthenticationServices

class LoginViewModel: ObservableObject {
    @Published var id: String = ""
    @Published var password: String = ""
    // TextFieldに入力した文字が半角英数かどうか判別する変数
    @Published var isValidId: Bool = false
    @Published var isValidPassword: Bool = false
    
    @Published var isLoginButtonTapped: Bool = false
    
    @Published var appleAuthResults: Result<ASAuthorization, Error>?
    
    private var disposables = [AnyCancellable]()
    
    init() {
        $id
            .sink(receiveValue: {
                self.isValidId = $0.isAlphanumeric && !$0.isEmpty ? true : false
            })
            .store(in: &disposables)
        
        $password
            .sink(receiveValue: {
                self.isValidPassword = $0.isAlphanumeric && !$0.isEmpty ? true : false
            })
            .store(in: &disposables)
        
        $isLoginButtonTapped
            .sink(receiveValue: { isTapped in
                if isTapped == true {
                    print("ここでログイン処理を呼び出す")
                }
            })
            .store(in: &disposables)
        
        $appleAuthResults
            .sink(receiveValue: { results in
                switch results {
                case .success(let authResults):
                    switch authResults.credential {
                    case let appleIDCredential as ASAuthorizationAppleIDCredential:
                        print("userIdentifier:\(appleIDCredential.user)")
                        print("fullName:\(String(describing: appleIDCredential.fullName))")
                        print("email:\(String(describing: appleIDCredential.email))")
                        print("authorizationCode:\(String(describing: appleIDCredential.authorizationCode))")
                        
                        print("ここでログイン処理を呼び出す")
                        
                    default:
                        break
                    }
                    
                case .failure(let error):
                    print(error.localizedDescription)
                    
                default:
                    break
                }
            })
            .store(in: &disposables)
    }
}

 

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

 

String+Extensions.swift

extension String {
    // 半角英数かどうか判別
    var isAlphanumeric: Bool {
        return !isEmpty && range(of: "[^a-zA-Z0-9]", options: .regularExpression) == nil
    }
}

 

参考

詳細! SwiftUI iPhoneアプリ開発入門ノート iOS 13 + Xcode11対応 | 大重 美幸 |本 | 通販 | Amazon

PEAKS(ピークス)|iOSアプリ設計パターン入門

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

https://help.apple.com/developer-account/#/dev04f3e1cfc

https://help.apple.com/developer-account/#/dev77c875b7e

https://help.apple.com/developer-account/#/devcdfbb56a3

https://developer.apple.com/forums/thread/656784