画像のリサイズ表示のハマりどころ

画像データをサイズを拡大縮小して表示する場合にハマるところです。

先に結論を語ります。.resizable モディファイアを忘れるなということです。これだけで問題が解決できると思います。

Image の画面表示サイズは .frame モディファイアで決定します。具体的には .frame( width: 所望の幅, height: 所望の高さ ) で設定します。しかし、これと同時に指定しなければならないモディファイアがあります。それが .resizable() です。

下記に、内部的な画像データが256*256のときの、画面出力サイズを変更した場合の実行例を示します。

.resizable モディファイアがない場合

Fig. 1 画面出力サイズ 256*256
Fig. 2 画面出力サイズ 128*512
Fig. 3 画面出力サイズ 512*128

なんだこれは!まったく意図した結果になりません。

.resizable モディファイアを指定した場合

Fig. 4 画面出力サイズ 256*256
Fig. 5 画面出力サイズ 128*512
Fig. 6 画面出力サイズ 512*128

よかった、意図どおりに実行されています。

実行コード

以下は、実際の実行コードです。@State な変数 FlagResizeEnable を評価して、59〜70行目で場合分けしています。

.resizable モディファイアを入れても入れなくても実行速度には関係ないようなので、つねに .resizable を指定しておいてもいいですね。

import SwiftUI

struct STRUCT_RGBA {
    var R: UInt8
    var G: UInt8
    var B: UInt8
    var A: UInt8
}

struct ContentView: View {

    // 画像データ.
    let DATA_W = 256
    let DATA_H = 256
    @State var Buffer: UnsafeMutableRawPointer?
    @State var DataRGBA: UnsafeMutablePointer<STRUCT_RGBA>?

    // 画像データを画面に表示するためのグッズ.
    @State var TheCGContext: CGContext? = nil
    @State var TheUIImage: UIImage? = nil

    // 画面に出力する際の縮尺.
    @State var RateX = 1.0
    @State var RateY = 1.0

    // 画面に出力するサイズ.
    @State var UiImgW: CGFloat = 0.0
    @State var UiImgH: CGFloat = 0.0

    // リサイズモディファイアの有効化フラグ.
    @State var FlagResizeEnable: Bool = true

    // ボタン関係の定数.
    let BTN_CORNER_RADIUS = 5.0
    let BTN_STROKE_LINE_WIDTH = 1.0
    let BTN_TXT_PADDING = 5.0
    let BTN_COLOR_FG = Color.white
    let BTN_COLOR_BG = Color.blue
    let BTN_FRAME_COLOR = Color.white

    // パスによる枠線描画の定数.
    let PATH_LINE_COLOR = Color.yellow
    let PATH_LINE_WIDTH = 2.0

    var body: some View {

        ZStack{

            // 最背面は背景グレーで塗りつぶす.
            Rectangle()
                .foregroundColor( .gray )

            // 中層面で画像を表示する.
            if let TheUIImageUnwrapped = TheUIImage {

                let ui_img_w = CGFloat( DATA_W ) * RateX
                let ui_img_h = CGFloat( DATA_H ) * RateY

                if FlagResizeEnable
                {
                    Image( uiImage: TheUIImageUnwrapped )
                        .resizable() // これが重要.
                        .frame( width: ui_img_w, height: ui_img_h )
                }
                else
                {
                    Image( uiImage: TheUIImageUnwrapped )
                        // こっちは resizable() しない.
                        .frame( width: ui_img_w, height: ui_img_h )
                }

                VStack{
                    Text( String( format: "%.1f*%.1f", ui_img_w, ui_img_h ) )
                        .foregroundColor( .white )
                    Text( String( format: "FlagResizeEnable %d", FlagResizeEnable ) )
                        .foregroundColor( .white )
                }

            }
            else
            {
                // TheUIImage が適切に生成されていなければ何も表示しない.
                Spacer()
            }

            // 最前面の画面上部にデバッグ情報を表示する.
            VStack{

                let scr_w = UIScreen.main.bounds.size.width
                let scr_h = UIScreen.main.bounds.size.height

                Text( String( format: "ScreenWH is %.1f*%.1f", scr_w, scr_h ))
                    .foregroundColor( .white )
                Text( String( format: "DataWH is %d*%d", DATA_W, DATA_H ))
                    .foregroundColor( .white )

            }
            .frame( maxWidth: .infinity, maxHeight: .infinity, alignment: .top )
            .padding( .top )

            VStack{

                HStack{

                    // 描画サイズ変更リクエストボタン.
                    Button( action : {
                        RateX = 0.5
                        RateY = 2.0
                    }) {
                        Text( "x0.5 x2.0" )
                            .padding( BTN_TXT_PADDING )
                            .foregroundColor( BTN_COLOR_FG )
                            .background( BTN_COLOR_BG )
                    }
                    .overlay(
                        RoundedRectangle( cornerRadius: BTN_CORNER_RADIUS )
                            .stroke( BTN_FRAME_COLOR, lineWidth: BTN_STROKE_LINE_WIDTH )
                    )

                    // 描画サイズ変更リクエストボタン.
                    Button( action : {
                        RateX = 1.0
                        RateY = 1.0
                    }) {
                        Text( "x1.0 x1.0" )
                            .padding( BTN_TXT_PADDING )
                            .foregroundColor( BTN_COLOR_FG )
                            .background( BTN_COLOR_BG )
                    }
                    .overlay(
                        RoundedRectangle( cornerRadius: BTN_CORNER_RADIUS )
                            .stroke( BTN_FRAME_COLOR, lineWidth: BTN_STROKE_LINE_WIDTH )
                    )

                    // 描画サイズ変更リクエストボタン.
                    Button( action : {
                        RateX = 2.0
                        RateY = 0.5
                    }) {
                        Text( "x2.0 x0.5" )
                            .padding( BTN_TXT_PADDING )
                            .foregroundColor( BTN_COLOR_FG )
                            .background( BTN_COLOR_BG )
                    }
                    .overlay(
                        RoundedRectangle( cornerRadius: BTN_CORNER_RADIUS )
                            .stroke( BTN_FRAME_COLOR, lineWidth: BTN_STROKE_LINE_WIDTH )
                    )

                }

                HStack {

                    // resizable 有効.
                    Button( action : {
                        FlagResizeEnable = true
                    }) {
                        Text( "resize enable" )
                            .padding( BTN_TXT_PADDING )
                            .foregroundColor( BTN_COLOR_FG )
                            .background( BTN_COLOR_BG )
                    }
                    .overlay(
                        RoundedRectangle( cornerRadius: BTN_CORNER_RADIUS )
                            .stroke( BTN_FRAME_COLOR, lineWidth: BTN_STROKE_LINE_WIDTH )
                    )

                    // resizable 無効.
                    Button( action : {
                        FlagResizeEnable = false
                    }) {
                        Text( "resize disalbe" )
                            .padding( BTN_TXT_PADDING )
                            .foregroundColor( BTN_COLOR_FG )
                            .background( BTN_COLOR_BG )
                    }
                    .overlay(
                        RoundedRectangle( cornerRadius: BTN_CORNER_RADIUS )
                            .stroke( BTN_FRAME_COLOR, lineWidth: BTN_STROKE_LINE_WIDTH )
                    )

                }

            }
            .frame( maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom )
            .padding( .bottom )

        } // ZStack end.
        .onAppear{

            // メモリを確保する.
            let numpix = DATA_W * DATA_H
            let size = MemoryLayout<STRUCT_RGBA>.stride * numpix
            let align = MemoryLayout<STRUCT_RGBA>.alignment
            Buffer = UnsafeMutableRawPointer.allocate( byteCount: size, alignment: align )

            // 連続メモリをRGBAの構造体の並びとして解釈する.
            DataRGBA = Buffer?.bindMemory( to: STRUCT_RGBA.self, capacity: numpix )

            // 確保したメモリに、ななめグラデーションデータを仕込む.
            SetGradDataRGBA( DataRGBA!, DATA_W, DATA_H )

            let one_scan_bytes = DATA_W * 4

            // ビットマップコンテキストを作成する.
            TheCGContext = CGContext(
                data: DataRGBA, // ここは data: Buffer, としても同じ動作になる.
                width: DATA_W,
                height: DATA_H,
                bitsPerComponent: 8,
                bytesPerRow: one_scan_bytes,
                space: CGColorSpaceCreateDeviceRGB(),
                bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
            )

            if TheCGContext != nil
            {
                let the_cgimage = TheCGContext!.makeImage()
                if let the_cgimage = the_cgimage
                {
                    TheUIImage = UIImage( cgImage: the_cgimage )
                }
                else
                {
                    // デバッグコンソールに出力する.
                    print( "the_cgimage is nil.")
                }
            }
            else
            {
                // デバッグコンソールに出力する.
                print( "the_cgcontext is nil." )
            }

        }
        .onDisappear {
            // メモリを破棄する.
            Buffer?.deallocate()
        }

    } // some View end.

    // 画像にデータを仕込むメソッド.
    func SetGradDataRGBA(
            _ data: UnsafeMutablePointer<STRUCT_RGBA>,
            _ width: Int,
            _ height: Int
            )->Void {

        let w = width
        let h = height

        var value: UInt8 = 0

        for j in 0 ..< h{
            var adrs = w * j
            for i in 0 ..< w{
                let tmp = i + j
                value = UInt8( tmp % 256 )
                data[ adrs ].R = 0x00;  // R
                data[ adrs ].G = value; // G
                data[ adrs ].B = value; // B
                data[ adrs ].A = 0xff;  // A
                adrs += 1
            }
        }

        return

    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}