【SwiftUI】UIImage / NSImage の Image への抽象化

どちらが好きですか、以下2つのコード。

 

■ 1つ目


import SwiftUI

public extension Image {
    #if canImport(AppKit)
    init(image: NSImage) {
        self = Image(nsImage: image)
    }
    #endif

    #if canImport(UIKit)
    init(image: UIImage) {
        self = Image(uiImage: image)
    }
    #endif
}

特徴:
- 拡張機能 (extension) を使って、Imageに新しいinitイニシャライザを追加しています。
- プラットフォームごとに異なる型 (NSImageやUIImage) を直接引数に取ります。
- プラットフォーム依存の条件付きで、NSImage(macOS)またはUIImage(iOS)を使用してImageを初期化しています。

メリット:
- 各プラットフォームに対応したイニシャライザが個別に用意されており、Imageの初期化が直感的です。

デメリット
- プラットフォームごとにinitメソッドが別々に定義されているため、共通の型を扱うのが難しい。

 

■ 2つ目


#if canImport(AppKit)
import AppKit
public typealias PlatformImage = NSImage
#elseif canImport(UIKit)
import UIKit
public typealias PlatformImage = UIImage
#endif

import SwiftUI

extension Image {
    init(platformImage: PlatformImage) {
        #if canImport(UIKit)
        self = Image(uiImage: platformImage)
        #elseif canImport(AppKit)
        self = Image(nsImage: platformImage)
        #endif
    }
}

特徴:
- PlatformImageという型エイリアスを使って、macOSのNSImageとiOSのUIImageを抽象化しています。
- platformImageという共通の引数型を持つイニシャライザを追加しています。これにより、プラットフォームごとにImageを初期化しますが、型エイリアスによって共通化されています。

メリット:
- 抽象化されているため、呼び出し側のコードがプラットフォームに依存しません。つまり、共通のコードでPlatformImage型を使えば、iOSでもmacOSでも同じコードで動作します。
- 可読性が高く、メンテナンスが容易です。プラットフォームごとにメソッドを分ける必要がなく、1つのメソッドで対応しています。

デメリット:
- プラットフォームごとに異なる処理を追加する際に、多少複雑になる可能性があります。

 

■ まとめ

AIによると、

結論:どちらが良いか?

2つ目のコードの方が一般的に推奨されます。理由は、コードの抽象化によって、呼び出し側がプラットフォームに依存しない形でImageを扱うことができるためです。メンテナンス性が高く、同じコードベースで複数のプラットフォームをサポートしやすくなります。

ただし、プラットフォームごとに異なる処理が必要なケースでは、1つ目のコードの方が直感的に分かりやすい場合もあるので、状況に応じて選択が変わることがあります。

ということです。

私的には、どっちも勉強になります、としか。



【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 バージョンなど、

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

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



【SwiftUI】吹き出しを作りたい

Instagram のこれ。

作ってみようと。

 

🧑🏻‍💻 popover で作る

使えそうなのでやってみました。

👉 popover(isPresented:attachmentAnchor:arrowEdge:content:) | Apple Developer Documentation



struct AnimatedSpeechBubble: View {
  @State private var show = false

  var body: some View {

    HStack {
      Text("お知らせ")
        .popover(isPresented: $show, arrowEdge: .trailing) {
          Text("横に出せないの?")
            .padding(.horizontal)
            .foregroundStyle(.background)
            .presentationBackground(.red)
            .presentationCompactAdaptation(.popover)
        }
      Spacer()
    }
    .frame(width: 250, height: 50)

    Button(show ? "hide" : "show") {
      show.toggle()
    }
    .buttonStyle(.borderedProminent)

  }
}

#Preview("animated") {
  AnimatedSpeechBubble()
    .padding()
    .frame(maxWidth: .infinity)
}

なぜか横 ( .trailing ) 方向に出すことができません。

GIFにしてみたら背景色もなんかあやしい。

あと、ボタンの色も勝手に変わる。

 

🧑🏻‍💻 手作りで

基本の組み合わせで作ります。

まず、吹き出しを作ります。

Path() は使いません。


struct SpeechBubble: View {
  var count: Int

  var body: some View {
    HStack(spacing: 0) {
      Rectangle()
        .fill(.red)
        .rotationEffect(.degrees(45))
        .frame(width: 20, height: 20)
        .offset(x: 14)
        .clipShape(.rect) // *
      HStack {
        Image(systemName: "heart.fill")
        Text("\(count)")
      }
      .foregroundStyle(.background)
      .padding()
      .background(.red, in: .rect(cornerRadius: 8))
    }
  }
}

#Preview("bubble") {
  SpeechBubble(count: 999)
}

アニメーションな部分は scaleopacity のトランジションを使います。


if show {
  SpeechBubble(count: 999)
    .transition(.scale(scale: 0.25).combined(with: .opacity))
}

いい感じです。

 

🧑🏻‍💻 まとめ

手作りでまあいけそうです。

以上のソースコード一式です。