iOS16時代のStoreKit使用法 (SwiftUI)

# はじめに

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95033/94d3fb2e-cded-4dd8-a67a-97a5edf90749.png" width=300>

SwiftUIで作成したアプリで、
ユーザーに評価やレビューを依頼するViewを表示しようと思ったのですが、
iOSのバージョンによって使い方がコロコロ変わっているのでまとめてみました。
(iPadOS, macOS, Mac Catalystでも事情は同じですが省略します)

# APIの変遷
* iOS 13.0 - <font color=#bf4800>(14.0でdeprecated)</font>
```Swift
SKStoreReviewController.requestReview()
```

* iOS 14.0 - <font color=#00af48>(deprecatedではない)</font>
```Swift
// 事前にUIWindowSceneを取得する必要がある。
SKStoreReviewController.requestReview(in: windowScene)
```

* iOS 16.0 以降
```Swift
@Environment(\.requestReview) var requestReview
requestReview()
```
UIWindowSceneを取得しなくても良くなった:clap::smiley: (面倒くさかった)

# コード
## EnvironmentValuesをカスタマイズする方法
iOS13 - 16まで可能な限りコードを共通化する。

```iOS15_requestReview```を```EnvironmentValues```に追加、
* iOS 16以降： ```RequestReviewAction```を返す。
* それ以外： ```iOS15_RequestReviewAction```を返す。

```iOS15_RequestReviewAction```の```callAsFunction()```で各OS用の```requestReview()```を呼ぶ。

※ iOS 15のサポートを終了したら```iOS15_...```を削除して、下記**カスタマイズを使用しない方法**のコードに変更する。

```Swift
import SwiftUI
import StoreKit

struct ContentView: View {

    @Environment(\.iOS15_requestReview) var requestReview
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
        }
        .onAppear {
            AppStore.perform(requestReview)
        }
    }
}

struct AppStore {
    @MainActor
    static func perform(_ action: Any) {
        if #available(iOS 16.0, *) {
            // iOS 16 -
            (action as? RequestReviewAction)?()
        } else {
            // iOS 13 - 15
            (action as? iOS15_RequestReviewAction)?()
        }
    }
}

@available(iOS, introduced: 13.0, obsoleted: 16.0, message: "Use RequestReviewAction instead")
@MainActor
struct iOS15_RequestReviewAction: EnvironmentKey, Sendable {
    static let defaultValue = Self()

    func callAsFunction() {
        if #available(iOS 14.0, *) {
            if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
                SKStoreReviewController.requestReview(in: scene)
            }
        } else {
            SKStoreReviewController.requestReview()
        }
    }
}

@available(iOS, introduced: 13.0, obsoleted: 16.0, message: "Use requestReview instead")
extension EnvironmentValues {
    @MainActor
    var iOS15_requestReview: Any {
        get {
            if #available(iOS 16.0, *) {
                return requestReview
            } else {
                return self[iOS15_RequestReviewAction.self]
            }
        }
    }
}
```


## カスタマイズを使用しない方法

### Minimum Deployments が iOS 16.0 以降
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95033/038a9bc6-1e8e-36d1-9d5f-cd7499e251a0.png" width=200>

```Swift
import SwiftUI
import StoreKit

struct ContentView: View {

    @Environment(\.requestReview) var requestReview

    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
        }
        .onAppear {
            requestReview()
        }
    }
}
```

---

### Minimum Deployments が iOS 13.0 - 15.x
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95033/5be2395b-d524-17f3-e9fc-dc98f059a746.png" width=200>

```Swift
import SwiftUI
import StoreKit

struct ContentView: View {

    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
        }
        .onAppear {
            if #available(iOS 14.0, *) {
                if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
                    SKStoreReviewController.requestReview(in: scene)
                }
            } else {
                SKStoreReviewController.requestReview()
            }
        }
    }
}
```

---

他に良い方法があったらご教授ください。:bow:


### ご注意
* requestReviewを呼び出すタイミングは真面目に考慮していません。
* UIWindowSceneの取得方法も真面目に考慮していません。



## 開発環境
* Xcode 14.1

## 参考
[Ratings and reviews (Human Interface Guideline)](https://developer.apple.com/design/human-interface-guidelines/patterns/ratings-and-reviews/)
[評価、レビュー、返答](https://developer.apple.com/jp/app-store/ratings-and-reviews/)
[SKStoreReviewController requestReview()](https://developer.apple.com/documentation/storekit/skstorereviewcontroller/3566727-requestreview)
[SKStoreReviewController  requestReview(in:)](https://developer.apple.com/documentation/storekit/skstorereviewcontroller/3566727-requestreview)
[RequestReviewAction](https://developer.apple.com/documentation/storekit/requestreviewaction)