[Swift/Firebase] In-App Messagingのカスタマイズ (UIKit)

SwiftUIバージョンの実装は以下を参考に、今回はUIKitでの実装です。

全パターンを網羅するのは手間なので今回はモーダルパターンのみの実装です。

[Swift/Firebase] In-App Messagingのカスタマイズ (SwiftUI)

実装

Firebase InAppの準備

以下を参考に、検証用のIDが必要です。

[Swift/Firebase] Firebase Installation IDのログを出す方法

カスタマイズ用のInAppモーダル管理ファイル


InAppMessagingCustomComponent.swift

import Firebase
import FirebaseInAppMessaging

private enum IAMDisplay {
    case unknown
    case card(InAppMessagingCardDisplay)
    case modal(InAppMessagingModalDisplay)
    case banner(InAppMessagingBannerDisplay)
    case imageOnly(InAppMessagingImageOnlyDisplay)

    init(_ messageForDisplay: InAppMessagingDisplayMessage) {
        switch messageForDisplay.type {
        case .card:
            self = (messageForDisplay as? InAppMessagingCardDisplay).map { .card($0) } ?? .unknown
        case .modal:
            self = (messageForDisplay as? InAppMessagingModalDisplay).map { .modal($0) } ?? .unknown
        case .banner:
            self = (messageForDisplay as? InAppMessagingBannerDisplay).map { .banner($0) } ?? .unknown
        case .imageOnly:
            self = (messageForDisplay as? InAppMessagingImageOnlyDisplay).map { .imageOnly($0) } ?? .unknown
        @unknown default:
            self = .unknown
        }
    }
}

final class InAppMessagingCustomComponent: InAppMessagingDisplay {
    func displayMessage(_ messageForDisplay: InAppMessagingDisplayMessage, displayDelegate: InAppMessagingDisplayDelegate) {
        DispatchQueue.main.async {
            displayDelegate.impressionDetected?(for: messageForDisplay)
            guard let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene else {
                return
            }
            guard let window = scene.windows.first(where: { $0.isKeyWindow }) else {
                return
            }
            // モーダルタイプの配信時にフロントで用意しているカスタムViewControllerを使用するようにする
            switch IAMDisplay(messageForDisplay) {
            case let .modal(modal):
                InAppMessagingModal.show(
                    on: window.rootViewController ?? UIViewController(),
                    title: modal.title,
                    body: modal.bodyText,
                    imageURL: URL(string: modal.imageData?.imageURL ?? ""),
                    buttonTitle: modal.actionButton?.buttonText,
                    buttonURL: modal.actionURL
                )
            case .card, .banner, .imageOnly, .unknown:
                // モーダルタイプ以外のタイプでの配信は何も表示しない
                return
            }
        }
    }
}


InAppMessagingModal.swift

import Foundation
import UIKit

struct InAppMessagingContents {
    let title: String
    let body: String?
    let imageURL: URL?
    let buttonTitle: String?
    let buttonURL: URL?
}

final class InAppMessagingModal {
    static func show(
        on viewController: UIViewController,
        title: String,
        body: String?,
        imageURL: URL?,
        buttonTitle: String?,
        buttonURL: URL?
    ) {
        let contents = InAppMessagingContents(
            title: title,
            body: body,
            imageURL: imageURL,
            buttonTitle: buttonTitle,
            buttonURL: buttonURL
        )
        let inAppMessagingModalViewController = InAppMessagingModalViewController(inAppMessagingContents: contents)
        viewController.present(inAppMessagingModalViewController, animated: true)
    }
}

InAppMessagingModalViewController.xib (Storyboardでも可)

  • 背面全体に閉じる用のButton
  • 右上の閉じるボタン (デザインは好きなものに)
  • タイトル用Label
  • ImageView
  • Body用Label
  • リンク遷移用のButton

InAppMessagingModalViewController.swift

import UIKit
import SafariServices

final class InAppMessagingModalViewController: UIViewController {
    // MARK: - Properties
    @IBOutlet private weak var titleLabel: UILabel!
    @IBOutlet private weak var imageView: UIImageView!
    @IBOutlet private weak var bodyLabel: UILabel!
    @IBOutlet private weak var messagingButton: DesignableButton!
    @IBOutlet private weak var rightTopCloseButton: UIButton!
    @IBOutlet private weak var backgroundCloseButton: UIButton!
    @IBOutlet private weak var inAppMessagingImageConstraint: NSLayoutConstraint!

    private let inAppMessagingContents: InAppMessagingContents

    // MARK: - Initialize
    init(inAppMessagingContents: InAppMessagingContents) {
        self.inAppMessagingContents = inAppMessagingContents
        super.init(nibName: Self.className, bundle: Self.bundle)
        self.modalPresentationStyle = .overFullScreen
        self.modalTransitionStyle = .crossDissolve
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: - View Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        titleLabel.text = inAppMessagingContents.title
        bodyLabel.text = inAppMessagingContents.body
        bodyLabel.isHidden = inAppMessagingContents.body == nil
        messagingButton.setTitle(inAppMessagingContents.buttonTitle, for: .normal)
        messagingButton.isHidden = inAppMessagingContents.buttonTitle == nil
        if let url = inAppMessagingContents.imageURL {
            setImage(from: url, to: imageView)
        }
    }
    @IBAction func backgroundButton(_ sender: Any) {
        dismiss(animated: true)
    }
    @IBAction func rightCloseButton(_ sender: Any) {
        dismiss(animated: true)
    }
    @IBAction func detailButton(_ sender: Any) {
        guard let url = inAppMessagingContents.imageURL else { return }
        let safariViewController = SFSafariViewController(url: url)
        present(safariViewController, animated: true)
    }

    func setImage(from url: URL, to imageView: UIImageView) {
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                print("Error downloading image: \(error)")
                return
            }
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                print("Invalid response from server")
                return
            }
            guard let data = data, let image = UIImage(data: data) else {
                print("Invalid image data")
                return
            }
            DispatchQueue.main.async {
                imageView.image = image
            }
        }
        task.resume()
    }
}

// MARK: - SFSafariViewControllerDelegate
extension InAppMessagingModalViewController: SFSafariViewControllerDelegate {
    func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
        dismiss(animated: true)
    }
}

AppDelegate

func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    FirebaseApp.configure()
    InAppMessaging.inAppMessaging().messageDisplayComponent = InAppMessagingCustomComponent()
    Installations.installations().installationID { id, error in
        if let error = error {
            print("Error retrieving installation ID: \(error.localizedDescription)")
        } else if let id = id {
            print("Firebase Installation ID: \(id)")
        }
    }
    return true
}

これで取得したIDを入れて配信すると表示されるようになります。