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 しておいて
- SwiftUI 側で
isPresentStoreProduct
を true にする。 - UIViewControllerRepresentable で
isPresentStoreProduct
が true になったら ViewController の AppStore を開くメソッドを実行する。 - UIViewController で、AppStore を開いた後、
isPresentStoreProduct
を false に戻す。- このとき、即時に false にすると UIViewControllerRepresentable 側で連続してイベントが発生してしまうため、 非同期に
DispatchQueue.main.async
で false にする。
- このとき、即時に false にすると UIViewControllerRepresentable 側で連続してイベントが発生してしまうため、 非同期に
動作させるとこんな感じになる。
ちなみに id 284602850 のアプリは、 Texas Hold’em である。
コードは GitHub にもあります。
https://github.com/daisuke-t-jp/SwiftUIPresentAppStoreProduct