【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つ目のコードの方が直感的に分かりやすい場合もあるので、状況に応じて選択が変わることがあります。

ということです。

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



【bash/zsh】検索付きリスト表示をターミナルで

iptv でテレビを見る。


モバイル、パソコンを問わずいろいろなOSで見ることができる。

ざっくり1分遅れで、気の利いた動画クライアントで見ることができるのだが、

GUI アプリでは、不具合時の状況が掴みづらい。

なので、ターミナル経由で見てみる。

👉 Roshan-R/termv: A terminal iptv player written in bash

この termv というアプリ。

ターミナル上の検索付き選択肢一覧リスト表示。

どうやって作ってるのか。

コードを見てみます。


SHELL="${BASH_BINARY}" \
            fzf -e -i --reverse --cycle --with-nth="1..-2"\
                --bind "enter:execute(_play {})"\
                --bind "double-click:execute(_play {})"\
                --header="Select channel (press Escape to exit)" -q "${*:-}" \
            < <( printf '%s\n' "${CHANNELS_LIST}" )

https://github.com/Roshan-R/termv/blob/0b7468d1bba239e50adc7e7693035f3652b9bec4/termv#L210

「fzf」というやつ。




多くの人が集まっています。


👉️ junegunn/fzf: :cherry_blossom: A command-line fuzzy finder

先人先生、今回も勉強になります。

いつもありがとうございます。


macOS 15 Sequoia 純正 Passwords アプリをメニューバーに入れる

macOS 15 Sequoia にアップグレードすると、

アプリケーションに Passwords のアイコンが表示されます。

純正のやつです。

余計なサードパーティのパスワード管理アプリは不要です。

メニューバーに入れておきましょう。

Passwords アプリを開いて「設定」から。

これでメニューバーから素早く起動できるようになります。

以下のようにショートカットで対応してたのですが便利になりました。



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

いい感じです。

 

🧑🏻‍💻 まとめ

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

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