LoginSignup
3
3

More than 1 year has passed since last update.

[SwiftUI] 元の画像のサイズを保持して画像とテキストを合成する

Last updated at Posted at 2022-12-06

概要

  • Viewを画像化する方法で、SwiftUI exporting the content of Canvasに書いてある方法だと画面の端末の表示サイズに合わせてしまうので、取得画像が小さくなる。また記事にある通りViewのframeが設定されていなければ画像を取得できない。
    そのため、ViewではなくUIImageからそのまま画像を描画してその上に画像化したTextを合成する方法にする。
  • iOS16からImageRendererが使えるようになっているのでこちらを使ってテキスト画像を作成する。
  • イメージピッカーはSwiftUIでまだ使えないのでSwiftUIでUIImagePickerControllerを使うにある通りUIViewControllerRepresentableを使った方法をそのまま使う。

環境

Xcode 14.1
iOS 16.0以上

画像合成処理の流れ

  1. 選択された画像の表示サイズを保持しておく
  2. 入力されたテキストを画像化
  3. テキスト画像を実際の画像のサイズに合わすように拡大する
  4. ドラッグした位置を調整して選択画像とテキスト画像を合成

できたもの

taki.gif
元画像の3000x2002を保持しています。

コード

メインの画面 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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3