BLOGサブスレッドの日常

2021.02.18

Xcode12/SwiftUIでSVGを使う

tama

tamaです。

SwiftUIいいですね!コードだけで画面を作れるのがとてもよいです。
そこはかとなくReactで関数コンポーネントを使って画面を作っていくのに似た印象を持っています。
そんなステキなSwiftUIですが、SVGを使うのにちょっとハマったので解決策を残しておきます。

Xcodeは(たぶん12から)SVGファイルを直接Assetsとして持つことができるようになりました。
以前はPDFに変換して持たせていたようですがその必要がなくなっています。
iOSのバージョンも13以降でしか対応していないようですが、SwiftUIを使う場合はiOS13以上のはず。

SVGをAssetsに組み込む方法

  1. Xcode 12 でAssets.xcassets に Image Set を追加します。名前はお好みで。
  2. プロパティを次の通り設定します。
    • ResizingPreserve Vector Data にチェックを入れる
    • ScalesSingle Scale にする
  3. All の点線の枠に表示したいSVGファイルをドロップします。

キレイに拡大して表示する方法

SwiftUIの Image ではどうがんばってもキレイに表示できませんでした、、(方法を見つけられていないだけかもしれない)
SwiftUIを使う方の頭には「困ったときの UIViewControllerRepresentable」という魔法の言葉があるかと思います。
今回もそれで解決します。

struct SVGImage: UIViewControllerRepresentable {
    let name: String

    func makeCoordinator() -> Coordinator {
        Coordinator(name: name)
    }

    func makeUIViewController(context: UIViewControllerRepresentableContext<SVGImage>) -> UIViewController {
        context.coordinator
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<SVGImage>) {
    }

    class Coordinator: UIViewController {
        let name: String

        init(name: String) {
            self.name = name
            super.init(nibName: nil, bundle: nil)
        }

        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        override func loadView() {
            view = UIImageView(image: UIImage(named: name))
        }
    }
}

UIImage()でリソースからロードしたSVGを UIImageViewUIViewControllerUIViewControllerRepresentable を用いて表示するだけのViewです。

    SVGImage(name: "logo.image")
        .frame(width: 500, height: 200)

のように呼んでやればキレイに拡大縮小することができます。

任意の色でSVGをレンダリングする方法

SF Symbolsのようにコードで色を指定してSVGイメージを描画することもできます。

SwiftUI の Image を使う場合はこんな感じです。

    Image("logo.image")
        .resizable()
        .renderingMode(.template)
        .foregroundColor(.green)
        .frame(width: 500, height: 200)

.renderingMode(.template) するかわりに Assetsのプロパティで Render AsTemplate Image にする方法もあります。
(Assetsのプロパティで指定するともともとSVGが持っていた色情報が使われなくなります)

キレイに拡大縮小しながら色も指定したい、、という欲張りさんにはこちら。

struct SVGImage: UIViewControllerRepresentable {
    let controller: Coordinator

    init(name: String) {
        controller = Coordinator(name: name)
    }

    func makeCoordinator() -> Coordinator {
        controller
    }

    func makeUIViewController(context: UIViewControllerRepresentableContext<SVGImage>) -> UIViewController {
        UIViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<SVGImage>) {
        uiViewController.view = controller.imageView
    }

    func scaledToFill() -> Self {
        controller.contentMode(.scaleAspectFill)
        return self
    }

    func scaledToFit() -> Self {
        controller.contentMode(.scaleAspectFit)
        return self
    }

    func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> Self {
        switch renderingMode {
        case .original:
            controller.renderingMode(.alwaysOriginal)
        case .template:
            controller.renderingMode(.alwaysTemplate)
        default:
            controller.renderingMode(.automatic)
        }
        return self
    }

    func imageColor(_ color: Color) -> Self {
        imageColor(color.uiColor())
    }

    func imageColor(_ color: UIColor) -> Self {
        controller.imageColor(color)
        return self
    }

    class Coordinator {
        let name: String
        let imageView = UIImageView()

        init(name: String) {
            self.name = name
            imageView.image = UIImage(named: name)
        }

        func contentMode(_ mode: UIView.ContentMode) {
            imageView.contentMode = mode
        }

        func renderingMode(_ renderingMode: UIImage.RenderingMode) {
            imageView.image = imageView.image?.withRenderingMode(renderingMode)
        }

        func imageColor(_ color: UIColor) {
            imageView.tintColor = color
            renderingMode(.alwaysTemplate)
        }
    }
}

(2020/02/19 コードを修正しました)

SwiftUI の ColorUIColor に変換する方法はこのあたり(1,2)を参考にしてください。
(私は Color.blue のように名前で指定されたときにちゃんと UIColor.systemBlue にマップされるようにswitch文を途中に入れました)

    SVGImage(name: "logo.image")
        .imageColor(.blue)
        .frame(width: 500, height: 200)

みたくすることでキレイに拡大縮小しながら色も指定することができます。

つまり、今回の結論は「困ったときの UIViewControllerRepresentable

この記事を書いた人

tama