Category: Uncategorized

  • SwiftUIアプリにLINEログインを追加、ユーザープロファイル情報の取得(.onOpenURLモディファイアを使用)

    この記事では、SwiftUIアプリでLineログインを統合し、ログイン成功時にユーザーのプロファイル情報を取得することについて説明します。

    SwiftUIアプリとは

    SwiftUIアプリはApp構造体のみを持ち、AppDelegate ファイルや SceneDelegate ファイルは持っていません。

    import SwiftUI
    
    @main
    struct TestApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    

    認証に成功した後、
    LineはアプリのカスタムURLスキーマを呼び出し、
    認証トークンの情報を提供します。

    SwiftUIアプリでは、
    onOpenURLモディファイアがあり、
    ユーザーがLineで認証した後のログイン結果を処理するために使用することができます。

    アプリの登録

    まず、LINEのデベロッパーサイトにアクセスし、アプリケーションを登録します。

    https://qiita.com/embed-contents/link-card#qiita-embed-content__fcf0925a32bcc0ead812484bd66d3b26

    まず、プロバイダを作成します。
    これは会社名でも自分の名前でもかまいません。
    プロバイダーは複数のチャンネル(アプリケーション)を含むことができます。

    Screenshot 2023-03-24 at 15.07.55.png

    次に、作成したプロバイダーをクリックし、アプリケーション(チャネル)を作成します。
    種類は、”LINEログイン”を選択します。

    Screenshot 2023-03-24 at 15.11.06.png

    新しいチャンネルのフォームで、
    情報を入力します。
    “アプリの種類”で、”ネイティブアプリ “をトグルします。

    Screenshot 2023-03-24 at 15.11.32.png

    リストに自分のチャンネルが表示されるので、クリックして開きます。

    Screenshot 2023-03-24 at 15.10.54.png

    「LINEログイン設定」にて、iOSアプリケーションのバンドルIDを入力します。

    Screenshot 2023-03-24 at 16.07.27.png
    Screenshot 2023-03-24 at 15.13.13.png

    ここで、「チャンネル基本設定」タブにある チャンネルID を覚えておきましょう

    Screenshot 2023-03-24 at 15.12.44.png

    また、チャンネルを公開とマークすることを確認してください。

    SDKを統合

    LineSDK はSwift Packageを使用しても配布されています。

    プロジェクトに追加するには、Xcodeの「File」メニューをタップし、「Add Packages」をクリックしてください。以下のURLを入力します:

    https://github.com/line/line-sdk-ios-swift.git
    Screenshot 2023-03-24 at 15.20.11.png

    そして、「LineSDK」にチェックを入れ、メインとなるiOSターゲットを選択し、フレームワークを追加します。

    Screenshot 2023-03-24 at 15.20.16.png

    URLタイプの追加

    ユーザーがLINEで認証されると、LINEはURLスキーマを使ってあなたのアプリを呼び出します。そのURLスキーマを定義する必要があります。
    プロジェクトファイルを開き、iOSアプリのターゲットを選択し、Info タブで、以下のURLスキーマを追加します。

    line3rdp.$(PRODUCT_BUNDLE_IDENTIFIER):

    Screenshot 2023-03-24 at 15.23.31.png

    SDKの初期化

    SwiftUIアプリのファイル(@main`があるファイル)内で、
    SDKをインポートし、
    アプリ起動時にLine SDKを初期化するためのAppDelegateアダプタを追加します。

    import SwiftUI
    +import LineSDK
    
    @main
    struct LearnEnglishGPTApp: App {
        
    +    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    
    +class AppDelegate: UIResponder, UIApplicationDelegate, UIWindowSceneDelegate {
    +    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    +        LineSDK.LoginManager.shared.setup(channelID: "ooooooooo", universalLinkURL: nil)
    +        return true
    +    }    
    +}
    
    

    channelIDには、チャンネルページのチャンネルIDです。

    認証結果の処理

    SwiftUIアプリの場合、
    認証結果を処理するためにonOpenURLを使用する必要があります。
    SwiftUIアプリのファイルに、
    ログインを処理するコードを追加します:

    import SwiftUI
    import LineSDK
    
    @main
    struct LearnEnglishGPTApp: App {
        
        @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
        var body: some Scene {
            WindowGroup {
                ContentView()
    +                .onOpenURL { openedURL in
    +                    if openedURL.absoluteString.contains("line3rdp") {
    +                        _ = LineSDK.LoginManager.shared.application(UIApplication.shared, open: openedURL)
    +                    }
                    }
            }
        }
    }
    
    class AppDelegate: UIResponder, UIApplicationDelegate, UIWindowSceneDelegate {
        
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
            LineSDK.LoginManager.shared.setup(channelID: "ooooooooo", universalLinkURL: nil)
            return true
        }
        
    }
    

    SwiftUI ビューにログインボタンを追加する

    ログインの状態を監視するために、observedオブジェクトを使用することにします。
    ここでの値は、ログインボタンを表示するビューに伝え返されます。
    また、より多くの情報を含むカスタム構造体を使用することもできます。

    class LineLoginStatus: NSObject, ObservableObject {
        @Published var isLoggingIn: Bool = false
        @Published var signInSuccessful_userID: String?
        @Published var errorMessage: String?
    }
    

    まず、SwiftUIでUIKit Lineログインボタンを表示するための互換ビューを作成する必要があります:

    import SwiftUI
    import LineSDK
    
    struct LineLoginButtonCompatibleView: UIViewRepresentable {
        
        @ObservedObject var stateManager: LineLoginStatus
        
        func makeUIView(context: Context) -> UIView {
            let containerView = UIView()
            let loginButton = LoginButton()
            loginButton.delegate = context.coordinator
            loginButton.permissions = [.profile, .openID]
            containerView.addSubview(loginButton)
            // auto layout
            loginButton.translatesAutoresizingMaskIntoConstraints = false
            let centerX = loginButton.centerXAnchor.constraint(equalTo: containerView.centerXAnchor)
            let centerY = loginButton.centerYAnchor.constraint(equalTo: containerView.centerYAnchor)
            NSLayoutConstraint.activate([centerX, centerY])
            return containerView
        }
        
        func updateUIView(_ view: UIView, context: Context) { }
        
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
        
        class Coordinator: NSObject, LoginButtonDelegate {
            
            var parent: LineLoginButtonCompatibleView
            
            init(_ parentView: LineLoginButtonCompatibleView) {
                self.parent = parentView
            }
            
            func loginButton(_ button: LoginButton, didSucceedLogin loginResult: LoginResult) {
                print("Line login success! \(loginResult.userProfile?.userID) \(loginResult.userProfile?.displayName) \(loginResult.accessToken.value)")
                parent.stateManager.signInSuccessful_userID = loginResult.userProfile?.userID
                parent.stateManager.isLoggingIn = false
            }
            
            func loginButton(_ button: LoginButton, didFailLogin error: LineSDKError) {
                print("Line Login Error \(error.localizedDescription)")
                parent.stateManager.isLoggingIn = false
                parent.stateManager.errorMessage = error.localizedDescription
            }
            
            func loginButtonDidStartLogin(_ button: LoginButton) {
                print("Line login started")
                parent.stateManager.isLoggingIn = true
            }
        }
        
    }
    
    Screenshot 2023-03-24 at 15.43.24.png

    ログインボタンを表示するビューにログインの状態を報告するために、LineLoginStatusを使用しています。
    その中で、
    @Published変数を使用し、
    これらの変数値の変更によってビューを更新します
    (そして.onChangeビューモディファイアを呼び出します)。

    ##SwiftUIビューにLINEのログインボタンを追加する

    さて、SwiftUIのビューにボタンを追加することができます:

    struct SignInSheet: View {
        
        @ObservedObject var lineLoginStatus: LineLoginStatus = .init()
        
        @State private var loggedInUserID: String?
        @State private var errorMessage: String?
        @State private var isProcessingLogin: Bool = false
        
        var body: some View {
            
            Form {
                
                Section("Button") {
                    LineLoginButton(stateManager: self.lineLoginStatus)
                        .onChange(of: self.lineLoginStatus.signInSuccessful_userID) { newValue in
                            if let newValue {
                                DispatchQueue.main.async {
                                    self.loggedInUserID = newValue
                                }
                            }
                        }
                        .onChange(of: self.lineLoginStatus.errorMessage) { newValue in
                            if let newValue {
                                DispatchQueue.main.async {
                                    self.errorMessage = newValue
                                }
                            }
                        }
                        .onChange(of: self.lineLoginStatus.isLoggingIn) { newValue in
                            DispatchQueue.main.async {
                                self.isProcessingLogin = newValue
                            }
                        }
                        .frame(height: 45)
                        .padding(.top, 5)
                }
                
                Section("Result") {
                    if isProcessingLogin {
                        ProgressView()
                    }
                    if let loggedInUserID {
                        Label(loggedInUserID, systemImage: "checkmark.circle")
                    }
                    if let errorMessage {
                        Label(errorMessage, systemImage: "xmark")
                    }
                }
                
            }
            
        }
    }
    

    onChange`ビューモディファイアを使って、ユーザーIDの変更(サインイン成功時)とエラーメッセージ(エラー時)を監視しています。

    これで、このアプリを実行し、ユーザーがログインに成功したときにユーザーIDを確認することができます。


    お読みいただきありがとうございました。

    🐘 マストドン @[email protected]

    ☺️ Twitter @MszPro

    ☺️ サイト https://MszPro.com

    writing-quickly_emoji_400.png

    Written by MszPro~

  • カメラからQRコードの検出、ハイライト表示、SwiftUI対応ビューの作成

    この記事では、カメラからのビデオストリームを表示し、
    QRコード(または他のタイプのコード)を検出し、
    その周りに矩形を表示することによってコードを強調するビューを作成することについて話します。
    また、SwiftUI互換のビューを作成し、SwiftUIのビュー内で使用できるようにします。

    変数を用意する

    まず、スキャンした結果とカメラプレビューレイヤーを保存するために、以下の変数を追加します。

    viewSize はカメラプレビューレイヤーのサイズを表します。

    プレビューレイヤーと検出されたQRコードのオーバーレイビューを初期化

    ビデオプレビューレイヤーを初期化し、サイズを設定します。

    qrCodeFrameView は、検出されたQRコードに重ねて表示される (オーバーレイ) ビューです。
    現在はフレームを持ちません。
    しかし、QRコードを検出したときに、そのビューの位置とサイズを設定することになります。

    ビデオ撮影用カメラデバイスを取得

    ビデオキャプチャーのためのデフォルトのカメラデバイスを取得します。
    そして、ビデオキャプチャセッションにカメラ入力を追加します。

    ** docatch ブロックの使い分けを忘れないように。

    メタデータ出力の追加(QRコード検出用)

    バーコードや物体検出にもVisionフレームワークを利用することができます。

    https://qiita.com/MaShunzhe/items/d2f82bd920eeb2e82167

    しかし、この機能はすでにAVFoundationフレームワークの中で提供されているので、代わりにAVCaptureMetadataOutputを使用することにします。

    AVCaptureMetadataOutput を使用すると、検出されたメタデータオブジェクトは setMetadataObjectsDelegate 関数で定義されたデリゲートに報告されます。

    metadataObjectTypes は、QRコードを探すためのコードを定義していますが、
    バーコードや他の多くの種類を検出するように設定することも可能です。

    ビデオセッションを開始

    ビデオのプレビューをレイヤーとしてビューに追加します(ユーザーは何がスキャンされているのか見ることができます)。

    その後、ビデオセッションの実行を開始します。

    また、qrCodeFrameView をビューに追加します。
    また、他のビューの上にオーバーレイビューを表示 (bringSubviewToFront) しています
    この時点では、サイズを持たないので、qrCodeFrameViewが画面には表示されません。

    メタデータ出力デリゲートを設定

    QRコードのメタデータが検出されたら、
    qrCodeFrameView のフレームを検出されたバーコードオブジェクトのバウンドに設定し、
    ビューの境界の色を緑に設定することができます。

    コードが検出されない場合は、
    フレームを0に設定してビューを非表示にすることができます。

    ** ここで、デバイスの向きの変更も検知し、それを利用してプレビューの向きを変更する必要の場合があります。

    SwiftUIでUIKitのビューを対応させる

    SwiftUIでUIKitの UIView を互換性のあるものにするために、
    UIViewRepresentableを使用することにします。

    初期化するために func makeUIView(context: Context) -> UIView 関数が必要です。

    デリゲート関数は Coordinator クラス内に配置されることになる。 class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate

    そして、func makeCoordinator() -> QRCodeScanner.Coordinator 関数内で Coordinatorを初期化します。

    また、変数の更新に基xづいてUIKitビューを更新する関数 func updateUIView(_ view: UIView, context: Context) を用意する必要があります。
    ここでは、スキャンしたQRコードの値を更新しているので、その関数内では何もする必要がありません。

    完成したSwiftUI互換のビューはこちらです。

    このコードをSwiftUIで使用するために

    また、Info.plistファイル内にカメラの使用の説明テキストを追加することを忘れないでください。


    お読みいただきありがとうございました。

    🐘 マストドン @[email protected]

    ☺️ Twitter @MszPro

    ☺️ サイト https://MszPro.com

  • Use iCloud CloudKit & Sign in with Apple to keep user’s information (UIKit)

    Are you using a third party database to keep user’s information? If you are only developing for the iOS platform, you can consider to use iCloud CloudKit with Sign in with Apple.

    What will we accomplish?

    1. Users can sign up your app with “Sign in with Apple”
    2. Users can sign in again on any of their devices (signed in with the same Apple ID) and you can retrieve the name, email, and other user information you saved.

    Let’s get started

    Step 1. Turn on the required capabilities in Xcode project settings. Turn on “CloudKit” in “iCloud”

    Now turn on “CloudKit”

    Click to toggle one of the existing “Containers” or click the plus icon to add a new one.

    Step 2. Set up the CloudKit data structure for storing the user information

    • Now click on “CloudKit Dashboard” button or go to http://icloud.developer.apple.com
    • Click the name of your application on the left side of the panel.
    • Click on Schema
    • Click on New Type

    Name the type “UserInfo” (same as the type you write in your code), select the type you just created.

    Click on “Add field button” on the right and add the following new fields:

    “emailAddress” and “name” are both “String”

    Step 3. Add the capability “Sign in with Apple”

    Now your “Capabilities” tab should contain at least these two:

    If you are lazy, the below codes can also be obtained here: https://github.com/mszopensource/CloudKitAppleSignInExample/blob/master/loginViewController.swift

    Step 4. Create a new login view controller (skip if you already did that)

    import Foundation
    
    import UIKit
    
    class loginViewController: UIViewController {
    
        override func viewDidLoad() {
    
            super.viewDidLoad()
    
        }
    
    }

    Import the necessary frameworks

    import AuthenticationServices
    
    import CloudKit

    Add a reference to the “Sign in with Apple button”

    @IBOutlet weak var signInBtnView: UIView!
    var authorizationButton: ASAuthorizationAppleIDButton!

    At here, I am adding thhe sign in button to an existing view called `signInBtnView`. `signInBtnView` has been added using UIStoryBoard here.

    Add that button to your view

    override func viewDidLoad() {
    
        super.viewDidLoad()
    
         authorizationButton = ASAuthorizationAppleIDButton(type: .default, style: .whiteOutline)
    
        authorizationButton.frame = CGRect(origin: .zero, size: signInBtnView.frame.size)
    
        authorizationButton.addTarget(self, action: #selector(handleAppleIdRequest), for: .touchUpInside)
    
        signInBtnView.addSubview(authorizationButton)
    
    }

    Handle button click action

    @objc func handleAppleIdRequest(){
    
        let appleIDProvider = ASAuthorizationAppleIDProvider()
    
        let request = appleIDProvider.createRequest()
    
        request.requestedScopes = [.fullName, .email]
    
        let authorizationController =     ASAuthorizationController(authorizationRequests: [request])
    
        authorizationController.delegate = self
    
        authorizationController.performRequests()
    
    }

    Set up the delegate to receive the result / error

    extension loginViewController: ASAuthorizationControllerDelegate {
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    
        //Success
    
    }
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
    
        //Failed
    
        print(error.localizedDescription)
    
    }

    Now you should have Sign in with Apple up and running. Test it now in your simulator!

    However, we still need to: 1. Obtain user information from the sign-in; 2. Keep or fetch from iCloud

    Fetch information from the sign-in

    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    
        if let appleIDCredential = authorization.credential as?    ASAuthorizationAppleIDCredential {
            let userID = appleIDCredential.user
            let name = appleIDCredential.fullName?.givenName
            let emailAddr = appleIDCredential.email
        }
    }

    Now here’s an important information:

    1. If it’s the first time a user uses “Sign in with Apple” with your app, you will receive the userID, name, and email address. We will work with CloudKit in later part of this article to save that information.
    2. If user is returning (signing in instead of signing up), then only the userID will be provided. We will work with CloudKit in later part of this article to learn how to fetch the name and email with the userID.
    3. Note that userID is always the same for one user

    Check if user is signing up (new user) or signing in

    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    
        if let appleIDCredential = authorization.credential as?  ASAuthorizationAppleIDCredential {
    
            let userID = appleIDCredential.user
    
            if let name = appleIDCredential.fullName?.givenName,
    
            let emailAddr = appleIDCredential.email {
    
                //New user (Signing up).
    
                //Save this information to CloudKit
    
            } else {
    
                //Returning user (signing in)
    
                //Fetch the user name/ email address
    
                //from private CloudKit
    
            }
    
        }
    
    }

    If there is a name and email provided (in other words, user is signing up), we can create a new CloudKit record:

    Note that we assign the ‘recordID’ to be the same as ‘userID’ we obtained from “Sign in with Apple” so that we can fetch user’s CloudKit record later.

    //New user (Signing up).
    
    //Save this information to CloudKit
    
    let record = CKRecord(recordType: "UserInfo", recordID: CKRecord.ID(recordName: userID))
    
    record["name"] = name
    
    record["emailAddress"] = emailAddr
    
    privateDatabase.save(record) { (_, _) in
    
    UserDefaults.standard.set(record.recordID.recordName, forKey: "userProfileID")

    If returning user, we will fetch the name and email:

    //Returning user (signing in)
    
    //Fetch the user name/ email address
    
    //from private CloudKit
    
    privateDatabase.fetch(withRecordID: CKRecord.ID(recordName: userID)) { (record, error) in
    
        if let fetchedInfo = record {
    
           let name = fetchedInfo["name"] as? String
    
            let userEmailAddr = fetchedInfo["emailAddress"] as? String
    
            //You can now use the user name and email address (like save it to local)
            UserDefaults.standard.set(name, forKey: "userName")
    
            UserDefaults.standard.set(userID, forKey: "userProfileID")
    
    }

    Completed code:

    mszopensource/CloudKitAppleSignInExample

    Also note that you’ll need to deploy the iCloud CloudKit database to production be you publish your app.

  • iOSアプリで画面上のQRコード部分のみ輝度を上げ(Metal, EDRレンダリング)

    WWDC 2022で発表されたMetalの新機能では、EDRを使って画面画像の一部分だけを明るく(現在設定されている画面の明るさ以上)することができます。

    部屋の明かりを消して最小限の明るさの画面

    背景

    QRコード決済画面を表示する場合、通常、画面全体の輝度が高くなり(夜間に開くと気分が悪くなることがある。。。)

    また、アプリ側で輝度の設定と復元を手動で行う必要があります。

    WWDC 2022で公開された新機能

    Metalフレームワークの新しいダイナミックEDR(Extended Dynamic Range)レンダリングサポートを使用すると、

    • QRコード画像のみを最大輝度で表示(ユーザーの現在の画面輝度設定に関係なく)
    • 画面の他の部分はユーザーが設定した明るさを維持
    • 画面表示の明るさを変更する必要がない
    • さらに、EDRを使えば、QRコードも画面の最大輝度より明るくすることができるので、より読み取りやすくなります。

    この記事では、この新機能をUIKitとSwiftUIの両方で使用することについて説明します。

    実装

    EDRはHDRと似ています。画像に保存されている輝度値を利用して、特定の画素を他の画素よりも明るくするものです。iPhoneの場合、輝度値が1より大きいと、現在設定されている画面の明るさよりも明るく表示されます。

    QRコードの CIImage は、明るさの値を画面がサポートする最大値に設定して作成することができます。
    そして、この画像をMetalフレームワークを使ってレンダリングし、iPhoneの画面のEDR機能を利用できるようにします。

    GitHubリポジトリ

    まずは試してみたいという方は、以下のソースコードをiPhoneで実行してみてください。

    効果をよりよく確認するために iPhoneの画面の明るさを最低にし、アプリを実行してください。

    シミュレータでは、EDRはサポートされていません。

    https://github.com/mszpro/QR-Code-EDR?ref=mszpro.com

    EDR対応テスト

    上記のように、EDRは画像上の特定のピクセルを、現在設定されている画面の明るさよりも明るく表示することができます。

    デバイスがそれをサポートしているかどうかは、UIScreen.main.currentEDRHeadroom または view.window?.screen.currentEDRHeadroom を使用してテストする必要があります。値が>1である場合、その端末はEDRをサポートしています。

    ステップ1.QRコードの作成

    まず、CIFilter の一種である CIQRCodeGenerator を用いてQRを作成する。

    inputMessage でQRコードの文字列内容を指定します。

    // ここでのコードは、ステップ6で使用されます
    
    guard let qrFilter = CIFilter(name: "CIQRCodeGenerator") else {
        return nil
    }
    
    let qrCodeContent = "testing-highlighted-qr"
    let inputData = qrCodeContent.data(using: .utf8)
    qrFilter.setValue(inputData, forKey: "inputMessage")
    qrFilter.setValue("H", forKey: "inputCorrectionLevel")
    
    guard let image = qrFilter.outputImage else {
        return nil
    }
    
    let sizeTransform = CGAffineTransform(scaleX: 10, y: 10)
    let qrImage = image.transformed(by: sizeTransform)
    

    ソースファイルに表示

    ステップ2.画面の最大輝度を計算する

    iPhoneは、それぞれ最大輝度の値が異なります。
    また、環境やバッテリー残量によって変化することもあります。

    ヘッドルーム headroom と呼ばれる、iPhoneの最大輝度値を取得する

    // ここでのコードは、ステップ6で使用されます
    
    let screen = view.window?.screen
    var headroom = CGFloat(1.0)
    if #available(iOS 16.0, *) {
        headroom = screen?.currentEDRHeadroom ?? 1.0
    }
    

    コンテンツが明るすぎるのが嫌な場合は、min で現在のヘッドルームと固定値(8など)の間の最小値を取得することができます。

    ステップ3.明るさの塗りつぶしレイヤーを生成する

    ここで、上記で算出した明るさの最大値で塗りつぶしレイヤーを生成します。
    ここでの色は通常の色とは異なり、明るさを表すもので、1.0より大きくすることができますので、ご注意ください。

    // ここでのコードは、ステップ6で使用されます
    
    let maxRGB = headroom
    guard let EDR_colorSpace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB),
          let maxFillColor = CIColor(red: maxRGB, green: maxRGB, blue: maxRGB,
                                     colorSpace: EDR_colorSpace) else {
        return nil
    }
    let fillImage = CIImage(color: maxFillColor)
    

    ソースファイルに表示

    ステップ4.QR画像と輝度レイヤーを合成する

    ここで、生成されたQRコードに明るさの値を与えて、最終的な画像を生成します。
    これを行うには、QR画像 qrImage をマスクとして使用し、塗りつぶしレイヤー fillImage(最大輝度のレイヤー)を切り取ります。

    // ここでのコードは、ステップ6で使用されます
    
    let maskFilter = CIFilter.blendWithMask()
    
    maskFilter.maskImage = qrImage
    maskFilter.inputImage = fillImage
    
    guard let combinedImage = maskFilter.outputImage else {
        return nil
    }
    
    return combinedImage.cropped(to: CGRect(x: 0, y: 0,
                                    width: 300 * scaleFactor,
                                    height: 300 * scaleFactor))
    

    ソースファイルに表示

    ステップ5.レンダラーをセットアップする

    さて、CIImage をレンダリングするレンダラーを作る必要があります。
    そして、レンダラーの設定で、EDRを使用するように指示します。

    まず、MTKViewDelegate 型に準拠した Renderer クラスを作成する。
    この Renderer クラスは、Metalビュー MTKView のデリゲートになります
    また、Metal で画像をレンダリングし、CIImage で作業するための適切なフレームワークをインポートする必要があります。

    import Metal
    import MetalKit
    import CoreImage
    
    class Renderer: NSObject, MTKViewDelegate, ObservableObject {
        
        let imageProvider: (_ contentScaleFactor: CGFloat, _ headroom: CGFloat) -> CIImage? // 画像データを提供する呼び出し側デリゲート関数
        
        public let device: MTLDevice? = MTLCreateSystemDefaultDevice()
        
        let commandQueue: MTLCommandQueue?
        let renderContext: CIContext? // 名前、キャッシュ環境設定、低電力設定の設定
        let renderQueue = DispatchSemaphore(value: 3) // 新しいフレームを描画する前に、前のレンダリングが完了するのを待つために使用される
        
        init(imageProvider: @escaping (_ contentScaleFactor: CGFloat, _ headroom: CGFloat) -> CIImage?) {
            self.imageProvider = imageProvider
            self.commandQueue = self.device?.makeCommandQueue()
            if let commandQueue {
                self.renderContext = CIContext(mtlCommandQueue: commandQueue,
                                           options: [.name: "QR-Code-Renderer",
                                                     .cacheIntermediates: true,
                                                     .allowLowPower: true])
            } else {
                self.renderContext = nil
            }
            super.init()
        }
        
        func draw(in view: MTKView) {
            // ToDo
        }
        
        func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
            // 描画可能なサイズや向きの変更に対応する。
        }
        
    }
    

    ソースファイルに表示

    renderContext 変数には、ログでのデバッグを容易にするため、レンダラーの名前を設定します。
    renderQueue 変数には、最大値 3 の DispatchSemaphore を設定します。

    ディスパッチセマフォ DispatchSemaphoreの使用

    これは、プログラムが次のレンダリングを開始する前に、前のレンダリングが完了するのを待つためのツールです。

    前のコマンドが終了するのを待つには、次のようにします。

    _ = renderQueue.wait(timeout: DispatchTime.distantFuture) は、前のタスクが終了するまで、プログラムは次のコードの行の実行を停止します。

    前のコマンドが終了したことをシステムに知らせるには、self.renderQueue.signal() を実行します。

    draw 関数を完成させる

    次に、コンテンツを描画する func draw(in view: MTKView) 関数に取り組みます。

    class Renderer: NSObject, MTKViewDelegate, ObservableObject {
        
        // ... 上記のコードは、変数とinit関数です。 //
        
        func draw(in view: MTKView) {
            
            guard let commandQueue else { return }
    
            // wait for previous render to complete
            _ = renderQueue.wait(timeout: DispatchTime.distantFuture)
            
            if let commandBuffer = commandQueue.makeCommandBuffer() {
                
                // コマンドが完了したら、次のフレームをレンダリングできるようにキューに通知する。
                commandBuffer.addCompletedHandler { (_ commandBuffer)-> Swift.Void in
                    self.renderQueue.signal()
                }
                
                if let drawable = view.currentDrawable {
                    
                    let drawSize = view.drawableSize
                    let contentScaleFactor = view.contentScaleFactor
                    let destination = CIRenderDestination(width: Int(drawSize.width),
                                                          height: Int(drawSize.height),
                                                          pixelFormat: view.colorPixelFormat,
                                                          commandBuffer: commandBuffer,
                                                          mtlTextureProvider: { () -> MTLTexture in
                        return drawable.texture
                    })
                    
                    // サポートされる最大EDR値(ヘッドルーム)を計算する
                    var headroom = CGFloat(1.0)
                    if #available(iOS 16.0, *) {
                        headroom = view.window?.screen.currentEDRHeadroom ?? 1.0
                    }
                    
                    // デリゲート関数から表示するCI画像を取得します。
                    guard var image = self.imageProvider(contentScaleFactor, headroom) else {
                        return
                    }
    
                    // ビューの可視領域で画像を中央に配置します。
                    let iRect = image.extent
                    let backBounds = CGRect(x: 0,
                                            y: 0,
                                            width: drawSize.width,
                                            height: drawSize.height)
                    let shiftX = round((backBounds.size.width + iRect.origin.x - iRect.size.width) * 0.5)
                    let shiftY = round((backBounds.size.height + iRect.origin.y - iRect.size.height) * 0.5)
                    image = image.transformed(by: CGAffineTransform(translationX: shiftX, y: shiftY))
                    
                    // 画像が透明の場合、背景を指定する
                    image = image.composited(over: .gray)
                    
                    // テクスチャーデスティネーションにレンダリングするタスクを開始します。
                    guard let renderContext else { return }
                    _ = try? renderContext.startTask(toRender: image, from: backBounds,
                                                      to: destination, at: CGPoint.zero)
                    
                    // レンダリング結果を表示し、レンダリングタスクをコミットする
                    commandBuffer.present(drawable)
                    commandBuffer.commit()
                }
            }
        }
        
        func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
            // 描画可能なサイズや向きの変更に対応する。
        }
        
    }
    

    ソースファイルに表示

    上記のコードでは、
    まず renderQueue.wait 関数を使用して、前のレンダリングが完了するのを待ちます。
    次に、コマンドバッファを取得し、描画のサイズを指定します。
    HDRで提供できる最大の明るさ(ヘッドルーム)を計算します。
    そして、CI画像オブジェクトをメタルビュー内でレンダリングし、レンダリング画像を中央に配置する。

    ステップ6.レンダラーを初期化する

    さて、QRコードと輝度値を含む生成したCIImage(手順1~手順4のコード)と、Rendererクラス(手順5)を元に、レンダラーを初期化します。

    let renderer = Renderer(imageProvider: { (scaleFactor: CGFloat, headroom: CGFloat) -> CIImage? in
        
        // QRコード画像を生成する
        guard let qrFilter = CIFilter(name: "CIQRCodeGenerator") else {
            return nil
        }
        
        let qrCodeContent = "testing-highlighted-qr"
        let inputData = qrCodeContent.data(using: .utf8)
        qrFilter.setValue(inputData, forKey: "inputMessage")
        qrFilter.setValue("H", forKey: "inputCorrectionLevel")
        
        guard let image = qrFilter.outputImage else {
            return nil
        }
        
        let sizeTransform = CGAffineTransform(scaleX: 10, y: 10)
        let qrImage = image.transformed(by: sizeTransform)
        
        // 空白の塗りつぶし画像を生成する
        let maxRGB = headroom
        let maxFillColor = CIColor(red: maxRGB, green: maxRGB, blue: maxRGB,
                                   colorSpace: CGColorSpace(name: CGColorSpace.extendedLinearSRGB)!)!
        let fillImage = CIImage(color: maxFillColor)
        
        // マスクフィルターを使って、最終的なQRコード画像を作成します。
        let maskFilter = CIFilter.blendWithMask()
        maskFilter.maskImage = qrImage
        maskFilter.inputImage = fillImage
        
        // ハイライトレイヤーとQR画像を合成する
        guard let combinedImage = maskFilter.outputImage else {
            return nil
        }
        return combinedImage.cropped(to: CGRect(x: 0, y: 0,
                                                width: 512.0 * scaleFactor,
                                                height: 384.0 * scaleFactor))
    })
    

    ソースファイルに表示

    ステップ7.メタルビューでQRコードを表示する

    それでは、メタルビューにQRコードを表示してみましょう。

    @StateObject var renderer: Renderer // 変数
    
    let metalView = MTKView(frame: .zero, device: renderer.device)
    
    // MetalKitを通じてCore Animationに、ビューの再描画頻度を提案する。
    metalView.preferredFramesPerSecond = 10
    
    // Core Imageがメタルコンピュートパイプラインを使用してビューにレンダリングできるようにします。
    metalView.framebufferOnly = false
    metalView.delegate = renderer
    
    if let layer = metalView.layer as? CAMetalLayer {
        // SDRより大きな値をサポートする色空間でEDRを有効にする。
        if #available(iOS 16.0, *) {
            layer.wantsExtendedDynamicRangeContent = true
        }
        layer.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearDisplayP3)
        // レンダービューがEDRのピクセル値をサポートしていることを確認する。
        metalView.colorPixelFormat = MTLPixelFormat.rgba16Float
    }
    

    上記のコードでは、
    新しいMetalビュー MTKView を初期化し、
    リフレッシュレート(フレーム毎秒)を10に設定し、
    Renderer インスタンスに delegate を設定し、
    EDR(Extended Dynamic Range)を使用するように設定しています。

    UIKitを使用している場合は、このビューをそのままUIKitのビューに追加することができます view.addSubview(metalView)

    SwiftUI互換のメタルレンダリングビューを作る

    アダプターを使うことで、SwiftUIと互換性のあるメタルビューを作ることもできます。

    import SwiftUI
    import MetalKit
    
    struct MetalView: ViewRepresentable {
        
        @StateObject var renderer: Renderer
        /// - Tag: MakeView
        func makeView(context: Context) -> MTKView {
            let view = MTKView(frame: .zero, device: renderer.device)
    
            // MetalKitを通じてCore Animationに、ビューの再描画頻度を提案する。
            view.preferredFramesPerSecond = 10
    
            // Core Imageがメタルコンピュートパイプラインを使用してビューにレンダリングできるようにします。
            view.framebufferOnly = false
            view.delegate = renderer
    
            if let layer = view.layer as? CAMetalLayer {
                // SDRより大きな値をサポートする色空間でEDRを有効にする。
                if #available(iOS 16.0, *) {
                    layer.wantsExtendedDynamicRangeContent = true
                }
                layer.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearDisplayP3)
                // レンダービューがEDRのピクセル値をサポートしていることを確認する。
                view.colorPixelFormat = MTLPixelFormat.rgba16Float
            }
            return view
        }
        
        func updateView(_ view: MTKView, context: Context) {
            configure(view: view, using: renderer)
        }
        
        private func configure(view: MTKView, using renderer: Renderer) {
            view.delegate = renderer
        }
    }
    

    ソースファイルに表示

    SwiftUIビューでレンダラーを使用する

    さて、初期化されたレンダラーのインスタンスと、上で作成した互換性のあるビューアダプターの両方を使用して、ハイライトされたQRコード画像を表示する単一のSwiftUIビューを作成することができます。

    import SwiftUI
    import CoreImage.CIFilterBuiltins
    
    /// - Tag: ContentView
    struct ContentView: View {
        var body: some View {
            // 独自のレンダラーを持つメタルビューを作成します。
            let renderer = Renderer(imageProvider: { (scaleFactor: CGFloat, headroom: CGFloat) -> CIImage? in
                
                // QRコード画像を生成する
                guard let qrFilter = CIFilter(name: "CIQRCodeGenerator") else {
                    return nil
                }
                
                let qrCodeContent = "testing-highlighted-qr"
                let inputData = qrCodeContent.data(using: .utf8)
                qrFilter.setValue(inputData, forKey: "inputMessage")
                qrFilter.setValue("H", forKey: "inputCorrectionLevel")
                
                guard let image = qrFilter.outputImage else {
                    return nil
                }
                
                let sizeTransform = CGAffineTransform(scaleX: 10, y: 10)
                let qrImage = image.transformed(by: sizeTransform)
                
                // 空白の塗りつぶし画像を生成する
                let maxRGB = headroom
                let maxFillColor = CIColor(red: maxRGB, green: maxRGB, blue: maxRGB,
                                           colorSpace: CGColorSpace(name: CGColorSpace.extendedLinearSRGB)!)!
                let fillImage = CIImage(color: maxFillColor)
                
                // マスクフィルターを使って、最終的なQRコード画像を作成します。
                let maskFilter = CIFilter.blendWithMask()
                maskFilter.maskImage = qrImage
                maskFilter.inputImage = fillImage
                
                // ハイライトレイヤーとQR画像を合成する
                guard let combinedImage = maskFilter.outputImage else {
                    return nil
                }
                return combinedImage.cropped(to: CGRect(x: 0, y: 0,
                                                width: 512.0 * scaleFactor,
                                                height: 384.0 * scaleFactor))
            })
    
            MetalView(renderer: renderer)
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    ソースファイルに表示

    ここで、iPhoneで実行すると、QRコードだけがハイライトされるのが見えます。

    また、異なる塗りつぶしレイヤーを生成することで、画像の異なる部分を強調することができます。また、明るさの値を1より小さくすることで、特定の部分をより暗くすることができます。


    お読みいただきありがとうございました。

    ☺️ Twitter @MszPro
    🐘 Mastodon @[email protected]