画像データ(符号なし8bits)を画面に表示する

符号なし 8bits データは、Swift では UInt8 型のデータとして定義されます。( C だと unsigned char 型、C# だと byte 型です )

このデータを画面に出す方法をご紹介します。流れとしては下記のようになります。

(0) 画像データ用のメモリを確保する。( Cなら malloc、C#なら new )
(1) 画像データに任意のデータを仕込む。
(2) 画像データから CGContext を作成する。( MFCなら DIB、C#なら Bitmap )
(3) CGContext から CGImage を作成する。
(4) CGImage を UIImage に変換する。( 変換であって作成ではないことに注意 )
(5) UIImage を UIImageView に表示する。( UIImageView は C# なら PictureBox )
(6) 不要になったら画像データ用のメモリを破棄する。

上記の (3) や (4) の操作は MFC や C# には無く、iOS の SwiftUI に特有の概念です。理由はよくわかりませんが、iPhone や iPad のようなタップ型の端末から MacBook のようなパソコン型の端末まで、同じコードを使ってビルドしようと思うと、そういった仕掛けが必要なのかもしれません。

以降は下記のコードの解説をします。

6、7行目、画像データのピクセルサイズ幅、高さを固定値で決定しています。
59行目、ZStack の .onAppear で画像データのメモリ確保をしています。
97 行目、ZStack の .onDisappear で画像データのメモリ破棄をしています。
62行目、任意のデバッグしやすいデータを格納してください。
65行目、画像データから CGContext を作成しています。bytesPerRow: space: bitmapInfo: の値に注目してください。
77行目、CGContext から CGIImage を作成しています。
80行目、無事に CGIImage が作成できたら、それを UIImage に変換します。
23行目、TheUIImage という UIImage が作成されていたら Image に表示します。作成されていなければ Spacer() です。それは何も表示しないことを意味します。
44、46行目、デバッグの手がかりとして変数を表示しています。

import SwiftUI

struct ContentView: View {

    // 画像データ.
    let DATA_W = 256
    let DATA_H = 256
    @State var Data000: UnsafeMutablePointer<UInt8>?

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

    var body: some View {

        ZStack{

            // 最背面は背景を塗りつぶす.
            Rectangle()
                .foregroundColor( .green )

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

                let ui_img_w = CGFloat( DATA_W )
                let ui_img_h = CGFloat( DATA_H )

                Image( uiImage: TheUIImageUnwrapped )
                    .frame( width: ui_img_w, height: ui_img_h )

            }
            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 )

        } // ZStack end.
        .onAppear{

            // メモリを確保する.
            let numpix = DATA_W * DATA_H
            let alloc_bytes = numpix
            Data000 = UnsafeMutablePointer<UInt8>.allocate(capacity: alloc_bytes)

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

            // ビットマップコンテキストを作成する.
            TheCGContext = CGContext(
                data: Data000,
                width: DATA_W,
                height: DATA_H,
                bitsPerComponent: 8,
                bytesPerRow: DATA_W,
                space: CGColorSpaceCreateDeviceGray(),
                bitmapInfo: 0
            )

            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 {
            // メモリを破棄する.
            Data000?.deallocate()
        }

    } // some View end.

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

        let w = width
        let h = height

        var value: UInt8 = 0

        for j in 0 ..< h{
            for i in 0 ..< w{
                let tmp = i + j
                value = UInt8( tmp % 256 )
                data[ i + w * j ] = value
            }
        }

        return

    }

}

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

iPhone14 の場合はスクリーンのサイズは、幅 390.0 で高さが 844.0 です。これの単位はポイント(pt)です。

それに対して、画像データのサイズは、幅が 256 で高さが 256 です。これの単位はピクセル(px)です。

本記事の画像データサイズは 256 × 256 ピクセルなので、1 : 1 で 256.0 × 256.0 ポイントの表示をしても、スクリーンサイズの 390.0 × 844.0 ポイントの内側に表示できるので特に問題はありません。

カンのいい人は 29 行目の .frame モディファイアで表示サイズを決定するのではないかと推測すると思います。しかしこれはハマリます。ドツボです。

ためしに

.frame( width: ui_img_w * 0.5, height: ui_img_h * 0.5 )

としてみてください。なんら画像の表示サイズが変わらないことがわかると思います。ここの 0.5 を 2.0 とか 4.0 などにして試してみてもいいですね。

表示サイズを変更するには、あとひとつモディファイアが必要です。この謎は下記の記事で解説しております。

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

画像データを画面に出力するときに .frame() モディファイアを使ってサイズ指定しますが、初心者が必ずハマるところを紹介します。

ハマらないでくださいね。