SwiftUI で SKStoreProductViewController を使って AppStore を表示する

SKStoreProductViewController を使うと、アプリから離脱せずに、AppStore で特定のアプリを表示することができる。これを使うと、アプリ内で他のアプリのインストールを促すことに使えたりする。

ただし、SKStoreProductViewController に delegate を設定するとき、SwiftUI ではそのままでは使えないので、UIViewControllerRepresentable を使ったパータンを考えてみる。(delegate を設定しない場合は、UIViewControllerRepresentable は不要で、そのまま使える)

まず、SKStoreProductViewController で AppStore を開く機能を持った ViewController を用意する。

import UIKit
import SwiftUI
import StoreKit

class StoreProductViewController: UIViewController {
    
    private var isPresentStoreProduct: Binding<Bool>
    private let appId: Int
    
    init(isPresentStoreProduct: Binding<Bool>, appId: Int) {
        self.isPresentStoreProduct = isPresentStoreProduct
        self.appId = appId
        
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
    }
    
    func presentStoreProduct() {
        let storeProductViewController = SKStoreProductViewController()
        storeProductViewController.delegate = self
        
        let parameters = [SKStoreProductParameterITunesItemIdentifier: self.appId]
        storeProductViewController.loadProduct(withParameters: parameters) { status, error -> Void in
            if status {
                self.present(storeProductViewController, animated: true, completion: nil)
            } else {
                if let error = error {
                    print("Error: \(error.localizedDescription)")
                }
            }
        }
        
        DispatchQueue.main.async {
            self.isPresentStoreProduct.wrappedValue = false
        }
    }
}


// MARK: - SKStoreProductViewControllerDelegate
extension StoreProductViewController: SKStoreProductViewControllerDelegate {
    func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {
        viewController.presentingViewController?.dismiss(animated: true, completion: nil)
    }
}

上記の ViewController の UIViewControllerRepresentable を用意する。

import SwiftUI

struct StoreProductViewControllerRepresentable: UIViewControllerRepresentable {
    typealias UIViewControllerType = StoreProductViewController
    
    var isPresentStoreProduct: Binding<Bool>
    let appId: Int
 
    func makeUIViewController(context: Context) -> UIViewControllerType {
        let viewController = StoreProductViewController(isPresentStoreProduct: isPresentStoreProduct,
                                                        appId: appId)
        return viewController
        
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        if self.isPresentStoreProduct.wrappedValue {
            uiViewController.presentStoreProduct()
        }
    }
}

作成した StoreProductViewControllerRepresentable を使った SwiftUI の View が以下になる。

import SwiftUI

struct ContentView: View {
    @State private var isPresentStoreProduct: Bool = false
    
    var body: some View {
        VStack {
            Button {
                self.isPresentStoreProduct = true
            } label: {
                Text("App Store Link")
            }
            
            
            StoreProductViewControllerRepresentable(isPresentStoreProduct: self.$isPresentStoreProduct,
                                                    appId: 284602850)
                .frame(width: 0, height: 0)
        }
    }
}

処理の流れはこうなる。

SwiftUI, UIViewControllerRepresentable, UIViewController 間で、Bool のフラグ isPresentStoreProduct を Binding しておいて

  1. SwiftUI 側で isPresentStoreProduct を true にする。
  2. UIViewControllerRepresentable で isPresentStoreProduct が true になったら ViewController の AppStore を開くメソッドを実行する。
  3. UIViewController で、AppStore を開いた後、 isPresentStoreProduct を false に戻す。
    • このとき、即時に false にすると UIViewControllerRepresentable 側で連続してイベントが発生してしまうため、 非同期に DispatchQueue.main.async で false にする。

動作させるとこんな感じになる。
ちなみに id 284602850 のアプリは、 Texas Hold’em である。

コードは GitHub にもあります。
https://github.com/daisuke-t-jp/SwiftUIPresentAppStoreProduct