SwiftUI・UIKit・AppKitでの画像処理の煩わしさを解消するためのヒント

図を作ってみました。

Appleの異なる画像処理フレームワーク(SwiftUI、UIKit、AppKit)間で画像データをやり取りする際のフロー図を示しています。

この図が示しているように、iOS や macOS で画像を扱う場合、複数の異なるフレームワーク(SwiftUI、UIKit、AppKit)間での画像データのやり取りが必要となることがよくあります。

しかし、それぞれのフレームワークは異なる画像型(UIKit では UIImage、AppKit では NSImage など)を使っているため、これらを統一して操作するのは煩雑です。

処理の煩わしさは以下の点にあります:

 

🧑🏻‍💻 異なる画像型の存在

UIKit、AppKit、Core Graphics などの各フレームワークはそれぞれ独自の画像データ型(UIImageNSImageCGImage など)を使います。

異なる画像型を相互に変換する必要があり、そのための変換処理が増えてしまいます。

 

🧑🏻‍💻 変換処理の多さ

SwiftUI の Image コンポーネントに画像を渡すには、UIKit の UIImage や AppKit の NSImage などの形式に適切に変換する必要があります。

例えば、UIImage を SwiftUI に渡すために Image(uiImage:) を使う必要があり、AppKit の NSImage なら Image(nsImage:) を使います。このように、変換手順が異なり、統一感に欠けます。

 

🧑🏻‍💻 データ型の制約と互換性

画像データをファイル形式(例えば、PNG や JPEG)に変換したり、ネットワークで送信する場合、Data 型を使う必要があります。

そのため、 UIImage.pngData()NSBitmapImageRep.representation(using:properties:) のような変換処理を追加で行わなければなりません。

こうした一連の変換処理は単純な画像表示のために多くの余分なコードを必要とし、開発者にとって煩わしいと感じる要因です。

 

🧑🏻‍💻 フレームワーク間の相違点

UIKit は iOS 向け、AppKitは macOS 向けのフレームワークなので、同じアプリをクロスプラットフォームで開発する際に、これらのフレームワーク間の違いに対応しなければならず、異なる API や処理方法に習熟する必要があります。

総じて、このような画像の処理は、単純に画像を表示・変換・送信したいだけであっても多くの手順が必要となるため、効率的でないことが多いです。

これが処理の煩わしさにつながっています。

 

🧑🏻‍💻 SwiftUI

Image(uiImage:) または Image(nsImage:) を使って、それぞれ UIKit の UIImage または AppKit の NSImage を表示できます。

ImageRenderer(content:) を使って、 UIImage または NSImage を作成することが可能です。

 

🧑🏻‍💻 UIKit の UIImage

SwiftUI の Image や AppKit の NSImage との間で画像の相互変換が可能です。

UIImagepngData() でPNGフォーマットのデータに変換できますし、 UIImage(data:) でデータから画像を生成できます。

 

🧑🏻‍💻 AppKit の NSImage

AppKit では NSImage.cgImage() で Core Graphics の CGImage を取得でき、NSBitmapImageRep クラスを使って画像表現の変換が行われます。

NSBitmapImageRep を通じて、NSImage は、PNGなどの形式でエクスポート可能です。

 

🧑🏻‍💻 Core GraphicsのCGImage

UIKit の UIImage や AppKitの NSImage と互換性があり、それらを通じて画像を描画・変換する基盤を提供します。

 

🧑🏻‍💻 Foundation の Data

UIImageNSImage から変換したデータ(例:PNG)を保持するデータ型。

これにより、画像データをファイルとして保存したり、ネットワークを通じて送信したりできます。

 

🧑🏻‍💻 したかったこと

AsyncImage 内での画像データの比較です。



302 リダイレクト後の画像を確認しています。

 

🧑🏻‍💻 まとめ

SwiftUI、UIKit、AppKit での画像処理は、それぞれのフレームワークが異なる画像形式を扱っているため、煩雑に感じることが多いです。

しかし、正しい変換手法を使い、CGImage などを活用すれば互換性を確保できます。

さらに、最新の API を使いこなすことで、画像処理を効率的に行い、開発の手間を減らすことが可能です。

この記事で紹介したヒントを参考に、より快適な画像処理の開発を目指しましょう。


【SwiftUI】Create Draggable Reorder ListView without List

👉 Drag and Drop List In SwiftUI. In this article, We will explore how to… | by Mobile Apps Academy | Medium

よくある UI の挙動を SwiftUI でどれだけシンプルに作れるのか。

やってみました。

本来は、何かを NSItemProvider() 経由で、

ドロップ先に渡すのが役目っぽいけども、

DropDelegate の便利さを利用して

配列を並び替えるイメージ。

並び替えのアニメーションは withAnimation デフォルトに頼る。

iOS と macOS、Preview と シュミレータ と 実機、OS バージョンなど、

互換しようとするといろいろありそう。

ここらのコンポーネントはまだ不安定な感じ ?



【Swift】FileManager を使いたい

最初、無駄にややこしくて使いづらい気がした。

しかし、整理できるとそうでもない。


let fileManager = FileManager.default
let documents = URL.documentsDirectory

Button("create file") {
  // new.txt
  let text = "Hello!"
  let to = documents.appending(component: "new.txt")
  try? text.write(to: to, atomically: true, encoding: .utf8)
}

Button("read file") {
  // new.txt
  let file = documents.appending(component: "new.txt")
  let text = (try? String(contentsOf: file, encoding: .utf8)) ?? "ERROR"
  print(text)
}

Button("create directory") {
  // some/
  let directory = documents.appending(component: "some/")
  try? fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
}

Button("copy file") {
  // new.txt - copy -> some/copied.txt
  let at = documents.appending(component: "new.txt")
  let to = documents.appending(component: "some/copied.txt")
  try? fileManager.copyItem(at: at, to: to)
}

Button("move file") {
  // new.txt - move -> some/moved.txt
  let at = documents.appending(component: "new.txt")
  let to = documents.appending(component: "some/moved.txt")
  try? fileManager.moveItem(at: at, to: to)
}

Button("copy directory") {
  // some/ - copy -> another/
  let at = documents.appending(component: "some/")
  let to = documents.appending(component: "another/")
  try? fileManager.copyItem(at: at, to: to)
}

Button("delete file") {
  // some/moved.txt
  let dir = documents.appending(component: "some/")
  let file = dir.appending(component: "moved.txt")
  try? fileManager.removeItem(at: file)
}

Button("delete directory") {
  // some/ and another/
  let dir1 = documents.appending(component: "some/")
  let dir2 = documents.appending(component: "another/")
  try? fileManager.removeItem(at: dir1)
  try? fileManager.removeItem(at: dir2)
}

つづいて、ディレクトリを指定するだけで、

そのディレクトリ内に存在するディレクトリとファイルの状態を表示できるようにしておきます。

実体の確認を頻繁にしやすくしておくこと大事。



extension FileManager {
  private func contents(directory url: URL) -> [URL] {
    (try? contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])) ?? []
  }

  func showContents(_ url: URL = .documentsDirectory) {
    print(
      String(format: "%@ %@", url.shortPath(), url.isFile ? "[\(url.fileSize)]" : "")
    )

    if url.isDirectory {
      contents(directory: url)
        .sorted(by: {
          let lr = [$0, $1].map {
            ($0.path().components(separatedBy: "/").dropLast().joined(), $0.path())
          }
          return lr[0] < lr[1]
        })
        .forEach { content in
          showContents(content)
        }
    }
  }
}


FileManager.default.showContents(.documentsDirectory)

// /HOME/Documents/ 
// /HOME/Documents/new.txt [6 bytes]
// /HOME/Documents/another/ 
// /HOME/Documents/another/copied.txt [6 bytes]
// /HOME/Documents/some/ 
// /HOME/Documents/some/copied.txt [6 bytes]
// /HOME/Documents/some/moved.txt [6 bytes]

👉 contentsOfDirectory(at:includingPropertiesForKeys:options:) | Apple Developer Documentation hatena-bookmark

これぐらいでいいか。

Permission とか mask の話しはまたそのうちやりたいです。



 

🤔 まとめ

コツとしては、URL の取り扱い。


ファイルやディレクトリは URL を使って指す。


URL を path() などを使って String に変換するのは表示直前のみ。


URL は実体の情報を保持していない。

という当たり前の言葉が思いつく。

しかし、最初は混乱したし、すると思う。

String のほうが直感的で人間に近いし、

古い API を見てると String ベースでファイルを操作してる。

フツーに検索しても古い記述がヒットすることが多い。

 

🤔 参考

👉 【Swift】ファイルやディレクトリ操作するための extension をまずは作った hatena-bookmark
👉 【Swift】URL で特定のディレクトリやファイルを指す hatena-bookmark