概要
- Viewを画像化する方法で、SwiftUI exporting the content of Canvasに書いてある方法だと画面の端末の表示サイズに合わせてしまうので、取得画像が小さくなる。また記事にある通りViewのframeが設定されていなければ画像を取得できない。
そのため、ViewではなくUIImageからそのまま画像を描画してその上に画像化したTextを合成する方法にする。 - iOS16からImageRendererが使えるようになっているのでこちらを使ってテキスト画像を作成する。
- イメージピッカーはSwiftUIでまだ使えないのでSwiftUIでUIImagePickerControllerを使うにある通り
UIViewControllerRepresentable
を使った方法をそのまま使う。
環境
Xcode 14.1
iOS 16.0以上
画像合成処理の流れ
- 選択された画像の表示サイズを保持しておく
- 入力されたテキストを画像化
- テキスト画像を実際の画像のサイズに合わすように拡大する
- ドラッグした位置を調整して選択画像とテキスト画像を合成
できたもの
コード
メインの画面 ContentView.swift
import SwiftUI
class ViewModel: ObservableObject {
@Published var uiImage: UIImage?
@Published var inputText = ""
@Published var frameSize: CGSize = .zero
@Published var offset: CGSize = .zero
}
struct ContentView: View {
@State private var isImagePickerViewPresented = false
@ObservedObject private var viewModel = ViewModel()
var body: some View {
let imageView = ImageView(viewModel: viewModel)
VStack {
imageView
HStack {
Button("画像選択") {
isImagePickerViewPresented.toggle()
}
.padding()
.sheet(isPresented: $isImagePickerViewPresented) {
ImagePickerView(image: $viewModel.uiImage, sourceType: .library)
}
TextField("テキスト", text: $viewModel.inputText)
.textFieldStyle(.roundedBorder)
.padding()
Button("保存") {
let composedImage = imageView.compose()
UIImageWriteToSavedPhotosAlbum(composedImage, nil, nil, nil)
}
.padding()
}
}
}
}
画像を表示するView ImageView.swift
- Textはドラッグ可能です。本当はpositionを使った方法にしたかったが
ImageRenderer
を使った画像化で位置の調整がめんどくさかったのでoffsetで調整しています。
そのためドラッグした位置の調整する記述がちょっと雑になっています。 - 画像合成時に保持したい変数はViewModelに、他は
@State
で使うようにしています。
import SwiftUI
struct ImageView: View {
@State private var offset: CGSize = .zero
@State private var dragging: CGSize = .zero
@ObservedObject var viewModel: ViewModel
var body: some View {
if let uiImage = viewModel.uiImage {
GeometryReader { proxy in
ZStack {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.onAppear {
viewModel.frameSize = proxy.size
}
textView
}
}
}
}
var textView: some View {
Text(viewModel.inputText)
.padding()
.border(.black)
.foregroundColor(.red)
.offset(
x: offset.width + dragging.width,
y: offset.height + dragging.height
)
.gesture(
DragGesture()
.onChanged { value in
dragging.width = value.translation.width
dragging.height = value.translation.height
}
.onEnded { value in
offset.width += dragging.width
offset.height += dragging.height
// 現在の画像オフセットをViewModelに保持
viewModel.offset = offset
dragging = .zero
}
)
}
/// 合成
@MainActor
func compose() -> UIImage {
guard let uiImage = viewModel.uiImage else { return UIImage() }
// テキスト画像を作成
let textImage = textView.getImage()
// 選択画像のサイズ
let imageSize = CGSize(width: uiImage.size.width, height: uiImage.size.height)
// 拡大倍率
let ratio: Double
if uiImage.size.height < uiImage.size.width {
ratio = uiImage.size.width / viewModel.frameSize.width
} else {
ratio = uiImage.size.height / viewModel.frameSize.height
}
// テキスト画像のサイズ
let textSize = CGSize(width: textImage.size.width * ratio, height: textImage.size.height * ratio)
// テキスト画像描画位置
let x = imageSize.width / 2 - textSize.width / 2 + viewModel.offset.width * ratio
let y = imageSize.height / 2 - textSize.height / 2 + viewModel.offset.height * ratio
let format = UIGraphicsImageRendererFormat()
format.scale = 1.0
return UIGraphicsImageRenderer(size: imageSize, format: format).image {_ in
// 画像を描画
uiImage.draw(in: CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height))
textImage.draw(in: CGRect(origin: CGPoint(x: x, y: y), size: textSize))
}
}
}
Extensions.swift
Viewをそのまま画像化するメソッドをextensionにする。
View内で使われている変数などは初期化されており、画像をドラッグして移動しても移動前の位置で画像が描画される。
import SwiftUI
extension View {
@MainActor
func getImage() -> UIImage {
let renderer = ImageRenderer(content: self)
renderer.scale = 1
return renderer.uiImage ?? UIImage()
}
}
悩んだところ
SwiftUI: 別Viewのメソッドを呼び出す ここの記事にある通り View内に別のインスタンスを作りそこから関数を呼ぶ方法は間違っていると思う。
let imageView = ImageView(viewModel: viewModel)
imageView
...
let composedImage = imageView.compose()
ただ、別Viewを画像化するメソッドがViewModelにあるのは違う気がする。ボタン押下後ViewModelから表示しているViewにどうやってアクセスすればいいかわからず。
どうすべきかわからなかった。
あとがき
- Textをそのまま画像化するとサイズは表示サイズのままなのでもっとカッコよくする場合は、テキストから画像にする際にフォントサイズの調整や、解像度が高い画像のテンプレートを用意しておくなど方法が必要だと思う。(Line cameraとかそんな感じ)
- 写真ライブラリのアクセスは
Privacy - Photo Library Usage Description
の記述をinfo.plistに忘れずに
参考
https://stackoverflow.com/questions/71285697/swiftui-exporting-the-content-of-canvas
https://zenn.dev/yorifuji/articles/swiftui-imagepicker
https://www.fuwamaki.com/article/65