SwiftUIでファイルから画像を読み込んで表示するmacOSアプリケーションの作成
概要
SwiftUIでファイルから画像を読み込みリサイズが画像の反転などを前処理を行って表示するアプリケーションを作成します。
環境
- macOS: Sequoia 15.5
- Xcode: 16.4
方法1:画像をAssetに登録して表示
あらかじめ用意しておいた画像を表示する場合はAssetに画像を登録して表示する方法だと簡単に実現できます。
Xcodeのプロジェクトに画像をドラッグ&ドロップするとAssetに画像を登録できます。

Assetに登録した画像はImageで表示できます。
struct ContentView: View {
var body: some View {
Image("dango")
}
}
しかし、画像がフレームより大きいと画像の表示が切れてしまいます。
以下のように設定するとアスペクト比を維持してフレームに合わせてリサイズして表示します。
struct ContentView: View {
var body: some View {
Image("dango")
.resizable()
.scaledToFit()
}
}

方法2:ファイルから画像を読み込む
例えば画像ビューアのようなアプリケーションの場合、ユーザーが用意した画像ファイルを読み込みたい場合があります。
iOSやmacOSではサンドボックスの仕組みでファイルへのアクセスが制限されています。
基本的にはユーザーが許可した場合にアクセスが可能となります。
ファイル選択のUIでユーザーが明示的に指定したファイル、ユーザーが許可を与えたファイルはアクセスできます。
Xcodeプロジェクト設定でSandboxの設定値が確認できます。

ファイル選択UIで指定した画像ファイルを開く
fileImporterを使えばファイルを選択するUIを表示して選択したファイルを処理する処理を記載することができます。
基本的な使い方はファイル選択UIの表示状態の変数の用意、ファイル選択UIを表示する処理、ファイル選択時の処理の記載となります。
以下のサンプルプログラムでは上記の説明以外にも状態管理のためにenumで状態の定義を追加して、読み込み中はグルグルを表示して、読み込み完了したら画像を表示するようにしています。
import SwiftUI
import UniformTypeIdentifiers
enum ImageState {
case idle
case loading
case displayed
}
struct ContentView: View {
@State var state: ImageState = .idle
@State var isPresented: Bool = false
@State var image: NSImage = NSImage()
var body: some View {
ZStack {
if (state == .idle) {
Button("Open Image") {
isPresented = true
}
.fileImporter(isPresented: $isPresented, allowedContentTypes: [.image], allowsMultipleSelection: false) { result in
state = .loading
switch result {
case .success(let urls):
guard let url = urls.first else { return }
let gotAccess = url.startAccessingSecurityScopedResource()
if !gotAccess { return }
image = NSImage(contentsOf: url) ?? NSImage()
url.stopAccessingSecurityScopedResource()
case .failure(let error):
print(error.localizedDescription)
}
state = .displayed
}
} else if (state == .loading) {
ProgressView()
} else {
ScrollView([.horizontal, .vertical]) {
Image(nsImage: image)
}
}
}
}
}

ドラッグ&ドロップで画像ファイルを表示
onDropを使うとドラッグ&ドロップされたファイルについても表示できるようになります。
基本的な使い方はドラッグ&ドロップの受付状態の変数の用意、ドラッグ&ドロップの受付状態を有効にする処理、ドラッグ&ドロップ時の処理の記載となります。
以下のサンプルプログラムではドラッグ&ドロップ受付時に画面を半透明の四角形領域を表示して視覚的に受付中であることが分けるようにしています。
import SwiftUI
import UniformTypeIdentifiers
enum ImageState {
case idle
case loading
case displayed
}
struct ContentView: View {
@State var state: ImageState = .idle
@State var isPresented: Bool = false
@State var image: NSImage = NSImage()
@State var isDropTargeted: Bool = false
var body: some View {
GeometryReader { geometry in
ZStack {
if (state == .idle) {
Button("Open Image") {
isPresented = true
}
.fileImporter(isPresented: $isPresented, allowedContentTypes: [.image], allowsMultipleSelection: false) { result in
state = .loading
switch result {
case .success(let urls):
guard let url = urls.first else { return }
let gotAccess = url.startAccessingSecurityScopedResource()
if !gotAccess { return }
image = NSImage(contentsOf: url) ?? NSImage()
url.stopAccessingSecurityScopedResource()
case .failure(let error):
print(error.localizedDescription)
}
state = .displayed
}
} else if (state == .loading) {
ProgressView()
} else {
ScrollView([.horizontal, .vertical]) {
Image(nsImage: image)
}
}
if isDropTargeted {
Rectangle()
.fill(Color.blue)
.opacity(0.2)
}
}
.frame(width: geometry.size.width, height: geometry.size.height)
}
.onDrop(of: [UTType.image.identifier], isTargeted: $isDropTargeted) { providers in
state = .loading
guard let provider = providers.first else { return false }
if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
provider.loadItem(forTypeIdentifier: UTType.image.identifier) { item, error in
guard error == nil, let url = item as? URL else { return }
image = NSImage(contentsOf: url) ?? NSImage()
}
}
state = .displayed
return true
}
}
}

画像をリサイズして表示
ここまでのサンプルプログラムではNSImageに画像の読み込み処理を任せていました。
ここでは内部的な画像データそのものを反転したりリサイズしたりして画像を前処理して表示する方法を記載します。
アプリケーションによってはフレームの表示方法を工夫して反転やサイズを変更することもできると思いますしその方が処理が高速な可能性はありますが、ここではあくまで画像データそのものを加工することを想定しています。
Rawデータの取得
以下のようにすると生データを取得できます。
do {
let rawData = try Data(contentsOf: url)
} catch let error {
print("error: \(error)")
}
その後画像を加工することになるのですが、NSImage画像から幅や高さを取得しようとすると実行する環境によってはディスプレイの設定によって取得できる値が変わってくる場合があります。
そのため画像そのものの幅と高さを取得するために以下のようにします。
do {
let rawData = try Data(contentsOf: url)
if let rep = NSBitmapImageRep(data: rawData) {
let width = rep.pixelsWide
let height = rep.pixelsHigh
}
} catch let error {
print("error: \(error)")
}
リサイズ処理
例えばリサイズで幅と高さを2倍にしたい場合は、幅と高さを2倍にした画像情報を作成して、新しいContextで画像を描画することでリサイズ後の画像データを生成します。
do {
let rawData = try Data(contentsOf: url)
if let rep = NSBitmapImageRep(data: rawData) {
let scale = 2.0
let newSize = NSMakeSize(CGFloat(rep.pixelsWide) * scale, CGFloat(rep.pixelsHigh) * scale)
// Make BitmapImageRep
let bitmapRep = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(newSize.width),
pixelsHigh: Int(newSize.height),
bitsPerSample: rep.bitsPerSample,
samplesPerPixel: rep.samplesPerPixel,
hasAlpha: rep.hasAlpha,
isPlanar: rep.isPlanar,
colorSpaceName: rep.colorSpaceName,
bytesPerRow: Int(newSize.width) * 4,
bitsPerPixel: rep.bitsPerPixel
)!
bitmapRep.size = newSize
// Make context
guard let context = CGContext(data: nil, width: Int(newSize.width), height: Int(newSize.height), bitsPerComponent: 8, bytesPerRow: Int(newSize.width) * 4, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return }
// Make image
guard let imageSource = CGImageSourceCreateWithData(rawData as CFData, nil) else { return }
guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else { return }
context.draw(cgImage, in: NSMakeRect(0.0, 0.0, newSize.width, newSize.height))
let newImage = context.makeImage()!
image = NSImage(cgImage: newImage, size: newSize)
state = .displayed
}
} catch let error {
print("error: \(error)")
}
画像の反転
上記でContextを生成できたのでContextに対して反転して描画されるように設定します。
do {
let rawData = try Data(contentsOf: url)
if let rep = NSBitmapImageRep(data: rawData) {
let scale = 2.0
let newSize = NSMakeSize(CGFloat(rep.pixelsWide) * scale, CGFloat(rep.pixelsHigh) * scale)
// Make BitmapImageRep
let bitmapRep = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(newSize.width),
pixelsHigh: Int(newSize.height),
bitsPerSample: rep.bitsPerSample,
samplesPerPixel: rep.samplesPerPixel,
hasAlpha: rep.hasAlpha,
isPlanar: rep.isPlanar,
colorSpaceName: rep.colorSpaceName,
bytesPerRow: Int(newSize.width) * 4,
bitsPerPixel: rep.bitsPerPixel
)!
bitmapRep.size = newSize
// Make context
guard let context = CGContext(data: nil, width: Int(newSize.width), height: Int(newSize.height), bitsPerComponent: 8, bytesPerRow: Int(newSize.width) * 4, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return }
// Flip image
let horizontalFlip = true
let verticalFlip = true
if (horizontalFlip && verticalFlip) {
context.translateBy(x: newSize.width, y: newSize.height)
context.scaleBy(x: -1.0, y: -1.0)
} else if (horizontalFlip) {
context.translateBy(x: newSize.width, y: 0)
context.scaleBy(x: -1.0, y: 1.0)
} else if (verticalFlip) {
context.translateBy(x: 0, y: newSize.height)
context.scaleBy(x: 1.0, y: -1.0)
} else {
context.translateBy(x: 0, y: 0)
context.scaleBy(x: 1.0, y: 1.0)
}
// Make image
guard let imageSource = CGImageSourceCreateWithData(rawData as CFData, nil) else { return }
guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else { return }
context.draw(cgImage, in: NSMakeRect(0.0, 0.0, newSize.width, newSize.height))
let newImage = context.makeImage()!
image = NSImage(cgImage: newImage, size: newSize)
state = .displayed
}
} catch let error {
print("error: \(error)")
}

Discussion