モバイルアプリの強みの一つは同じデバイスでSNSやメッセージアプリで手軽にコンテンツを共有できることです。そこで今回はアプリ内で少し複雑なシェア用の画像を生成する方法を紹介します。
ゲームのリザルト画面やクーポンなど様々なケースが考えられますが、実例として私が開発しているAIキャラ制作アプリ『Eveki』の場合を挙げて実装までを考えてみます。
実装したい機能
ユーザー体験
『Eveki』ではユーザーが話し相手となるAIの設定を行うことできます。これに加えて、作ったAIをコンテンツとして共有できるようにしたいと考えています。具体的には、情報を反映させたカードを画像として生成し、友人に送信したりコレクションすることができるようにします。
課題の整理
画像生成というと難しく聞こえますが、まずは問題を分割してみましょう。コードの段階から画像を編集する操作は複雑になりそうです。しかし、ユーザーの入力に合わせてView
の見た目を変化させることは簡単です。
そこでこのカード部分のView
を部分的にスクリーンショットし、これを画像としてユーザー側に渡せるようにします。
- カードを
View
として表示 - この
View
をスクリーンショット - 画像を保存または共有
Viewの作り方
これは説明する必要がないと思うので省略しますが、後述する方法ならほとんどのView
には対応できます。Image
を使えば背景などに画像を組み込むこともでききます。自由度も高く、これを活かしてデザイナーさんに腕を振るってもらうのもいいかもしれません。
スクリーンショット
iOS16から
その名の通り画像レンダリングを行うImageRenderer
を使うことができます。
import SwiftUI
struct ContentView: View {
// ここでは、共有する画像を管理するための変数shareImageを定義しています。
@State private var shareImage: UIImage? = nil
var body: some View {
VStack {
CardView()
// "render"というタイトルのボタンを作成します。このボタンを押すと、renderメソッドが呼び出されます。
Button("render") {
render()
}
}
}
// renderメソッドは、shareViewを画像としてレンダリングします。
// ここでは使いませんが.cgImageも選択可能です
@MainActor
func render() {
let renderer = ImageRenderer(content: shareView())
shareImage = renderer.uiImage
}
}
UIViewRepresentableを使う
iOS16以前でもこれと似た動きを実装することができます。まずはUIViewRepresentableを作りましょう。
import SwiftUI
/// `ScreenshotMaker`を引数とするクロージャー
typealias ScreenshotMakerClosure = (ScreenshotMaker) -> Void
/// `ScreenshotMaker`ビューのSwiftUI表現
struct ScreenshotMakerView: UIViewRepresentable {
let closure: ScreenshotMakerClosure
/// クロージャーを使用して`ScreenshotMakerView`を初期化します
init(_ closure: **@escaping** ScreenshotMakerClosure) {
self.closure = closure
}
func makeUIView(context _: Context) -> ScreenshotMaker {
let view = ScreenshotMaker(frame: CGRect.zero)
return view
}
func updateUIView(_ uiView: ScreenshotMaker, context _: Context) {
DispatchQueue.main.async {
closure(uiView)
}
}
}
/// スクリーンショットを撮るためのカスタムUIViewサブクラス
class ScreenshotMaker: UIView {
/// このビューの親の、さらに親ビューのスクリーンショットを撮ります
/// - Returns: ビューのスクリーンショットを含むUIImage
func screenshot() -> UIImage? {
guard let containerView = superview?.superview,
let containerSuperview = containerView.superview else { return nil }
let renderer = UIGraphicsImageRenderer(bounds: containerView.frame)
return renderer.image { context in
containerSuperview.layer.render(in: context.cgContext)
}
}
}
extension View {
/// ビューにスクリーンショットのオーバーレイを追加します
/// - Parameter closure: スクリーンショットを撮る際に実行されるクロージャー
/// - Returns: スクリーンショットのオーバーレイが追加された変更されたビュー
func screenshotView(_ closure: @escaping ScreenshotMakerClosure) -> some View {
let screenshotView = ScreenshotMakerView(closure)
return overlay(screenshotView.allowsHitTesting(false))
}
}
管理用のScreenshotMaker
クラスを使う必要はありますが、ほとんど同じような形で実装することができます。ちなみに画像のサイズや画質などについては両方の手法に大きな差は見られませんでした。
import SwiftUI
struct ContentView: View {
@State var screenshotMaker: ScreenshotMaker?
@State private var shareImage: UIImage? = nil
var body: some View {
VStack {
CardView()
Button("render") {
render()
}
}.screenshotView { screenshotMaker in
self.screenshotMaker = screenshotMaker
}
}
func render() {
shareImage = screenshotMaker?.screenshot()
}
}
共有・保存
iOS16から
ShareLink
を使うことができます。ただし、これはURLをシェアする目的が主のようなので、画像を共有するにはTransferable型に準拠した独自の型を作る必要があります。共有時のシートに表示されるタイトルや説明などは
import SwiftUI
struct ScreenshotItem: Transferable {
static var transferRepresentation: some TransferRepresentation {
// ここが共有するのはimageであると伝える役割
ProxyRepresentation(exporting: \.image)
}
public var image: Image
public var caption: String
}
struct ContentView: View {
@State private var photo = ScreenshotItem(image:
Image("appicon"),caption: "sample")
var body: some View {
VStack {
CardView()
.onAppear {
render()
}
ShareLink(
item: photo,
//以下でシート上での表示や説明を変更
subject: Text("Evekiカード"),
message: Text("共有してみよう!"),
preview: SharePreview(
photo.caption,
image: photo.image)
)
}
}
func render() {
DispatchQueue.main.async {
let renderer = ImageRenderer(content: shareView())
if let uiImage = renderer.uiImage {
photo.image = Image(uiImage: uiImage)
}
}
}
}
UIKitを使う
UIActivityViewController
をSwiftUIに対応させる形をとります。ただシート上での表示を細やかに切り替えるにはLinkPresentation
フレームワークが必要です。
import SwiftUI
import LinkPresentation
class ShareActivityItemSource: NSObject, UIActivityItemSource {
var shareText: String
var shareImage: UIImage
var linkMetaData = LPLinkMetadata()
init(shareText: String, shareImage: UIImage) {
self.shareText = shareText
self.shareImage = shareImage
linkMetaData.title = shareText
super.init()
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return UIImage(named: "AppIcon ") as Any
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return nil
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
return linkMetaData
}
}
struct ShareSheet: UIViewControllerRepresentable {
@Binding var image: UIImage
let text = "Evekiカード"
func makeUIViewController(context: Context) -> UIActivityViewController {
let itemSource = ShareActivityItemSource(shareText: text, shareImage: image)
let activityViewController = UIActivityViewController(activityItems: [itemSource], applicationActivities: nil)
return activityViewController
}
func updateUIViewController(_ vc: UIActivityViewController, context: Context) {
}
}
基本的には以下のよう呼び出すだけで使えます。ただiOS16の場合と異なり、画面いっぱいのモーダル画面が表示されます。機能としては問題ありませんが、もし気になる場合はこの記事が修正の参考になります。
struct ContentView: View {
//省略
@State private var showSheet = false
var body: some View {
//省略
Button("Share") {
showSheet.toggle()
}.sheet(isPresented: $showSheet) {
ShareSheet(image: $shareImage)
}
}
}
さいごに
スクリーンショットと共有機能を組み合わせれば、アプリ上で画像生成を行うことが可能です。最近は情報の記録をスクリーンショットで行う人も多いと聞きます。また、画像の投稿がきっかけで話題になるアプリやサービスも増えています。万が一アプリを削除されてしまっても、カメラロールに痕跡を残しておけるというメリットもあります。発想次第では分野を問わず活用できると思うので、ぜひ一度試してみてください。