【SwiftUI】Apple 公式サンプルでは Animation と Transition をどのように組み合わせているのか

画面上にメッセージを表示する View。


import SwiftUI

struct Message: View {
  var text: String

  var body: some View {
    HStack(spacing: 0) {
      Image(systemName: "heart.fill")
        .foregroundStyle(.red)
        .font(.title)
        .padding()
      Text(text)
        .padding(.trailing, 24)
    }
    .clipShape(.capsule)
    .background(
      .regularMaterial.shadow(.drop(radius: 16)),
      in: .capsule
    )
  }
}

#Preview {
  Message(text: "Hello, world!")
}

どのように、アニメーションやトランジションをつけて、生き生きとした画面にしているか。

Apple 公式サンプルを参考に書いてみます。

 

🧑🏻‍💻 Preview 用 View の準備

挙動を何度も確かめるために、

Preview 専用の View を作っておきます。


private struct RefreshPreview: View {
  var text: String

  @State private var id = false

  var body: some View {
    Message(text: text)
      .id(id)

    Button("Refresh") {
      id.toggle()
    }
    .buttonStyle(.borderedProminent)
  }
}

👉 【SwiftUI】View の 強制再描画 hatena-bookmark

ボタンを押すと強制的に画面再描画がされて、

表示開始からの動きを確認できるようになります。

 

🧑🏻‍💻 まずは アニメーション・トランジションなしでつくる


import SwiftUI

struct Message: View {
  var text: String

  @State private var showIcon = false
  @State private var showText = false

  var body: some View {
    HStack(spacing: 0) {
      if showIcon {
        Image(systemName: "heart.fill")
          .foregroundStyle(.red)
          .font(.title)
          .padding()
      }
      if showText {
        Text(text)
          .padding(.trailing, 24)
      }
    }
    .clipShape(.capsule)
    .background(
      .regularMaterial.shadow(.drop(radius: 16)),
      in: .capsule
    )
    .frame(height: 50)
    .onAppear {
      Task {
        showIcon = true
        try await Task.sleep(for: .seconds(1))
        showText = true
        try await Task.sleep(for: .seconds(1))
        showText = false
        try await Task.sleep(for: .seconds(1))
        showIcon = false
      }
    }
  }
}

private struct RefreshPreview: View {
  var text: String

  @State private var id = false

  var body: some View {
    Message(text: text)
      .id(id)

    Button("Refresh") {
      id.toggle()
    }
    .buttonStyle(.borderedProminent)
  }
}

#Preview {
  RefreshPreview(text: "Hello, world !!!")
    .padding()
    .frame(maxWidth: .infinity)
}

アイコン画像とテキスト部分をそれぞれの @State で


アイコン表示 

  ↓

テキスト表示

  ↓

テキスト非表示

  ↓

アイコン非表示

と Task の中で1秒ごとに変化させています。

しかし、アニメーションやトランジションがないので、

スムーズに View が変化しません。

 

🧑🏻‍💻 アニメーション・トランジションをつける



 

🧑🏻‍💻 まとめ

やっぱ、全然違いますね。

👉 sample-backyard-birds/Multiplatform/Birds/BirdFoodHappinessIndicator.swift at main · apple/sample-backyard-birds hatena-bookmark

 

🧑🏻‍💻 追記: 上記のトランジションっている ?

withAnimation() 記述は必要だとして、

transition() 記述は不要なのではないか。

できるだけシンプルにテンプレート化しておきたいので、

0.25 倍の速度で確認してみます。

上がトランジションなし、

下がトランジションあり。

トランジションはあったほうがいいですね。

やっぱり、Apple 公式サンプルコードは偉大。



【macOS】 アプリのすっきりアンインストール方法から学ぶ「アプリ関連ファイルの場所」

いまさら調べてみる。まず Apple 公式。


* Lauchpad からアンインストールする

* Finder を使って
  - ゴミ箱にいれる
  - ⌘ ( command ) + ⌫ ( delete )

* アプリ同梱のアンインストールスクリプト

👉 Mac でアプリをアンインストールする - Apple サポート (日本) hatena-bookmark

コマンドラインで。


❯ sudo uninstall file://Applications/Pages.app

👉 How to completely uninstall an app on a Mac and delete all junk files hatena-bookmark

Homebrew のコマンドを使うなら。


❯ brew help uninstall
Usage: brew uninstall, remove, rm [options] installed_formula|installed_cask
[...]

Uninstall a formula or cask.

  -f, --force                      Delete all installed versions of formula.
                                   Uninstall even if cask is not installed,
                                   overwrite existing files and ignore errors
                                   when removing files.
      --zap                        Remove all files associated with a cask.
                                   May remove files which are shared between
                                   applications.
      --ignore-dependencies        Don't fail uninstall, even if formula is a
                                   dependency of any installed formulae.
      --formula, --formulae        Treat all named arguments as formulae.
      --cask, --casks              Treat all named arguments as casks.
  -d, --debug                      Display any debugging information.
  -q, --quiet                      Make some output more quiet.
  -v, --verbose                    Make some output more verbose.
  -h, --help                       Show this message.

実際、今現在、私も使っているが、このアプリがベストに思える。


👉 AppCleaner hatena-bookmark

サードパーティのアプリなのですが、

どのファイルを削除してるのだろうか

と思いました。

 

🤔 手動で完全にアンインストールする方法

実際にはやらないけども、アプリの構造やインストーラのしくみの理解にもつながるはず。

👉 t18n/uninstall-cli: Open-sourced CLI tool to uninstall application on MacOS hatena-bookmark

アプリ(.app)のパス

から

「アプリ名」と「アプリ識別子」

を取得してから、

可能性のあるディレクトリを走査して、

見つけたファイルが削除の対象。

キャッシュやテンポラリなディレクトリも操作しています。


❯ app=/Applications/Numbers.app

❯ /usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "$app/Contents/Info.plist"
com.apple.iWork.Numbers

❯ echo $(basename $app .app)
Numbers

❯ find "$HOME/Library/Application Scripts" -iname "*com.apple.iWork.Numbers*" -maxdepth 1 -prune
/Users/me/Library/Application Scripts/com.apple.iWork.Numbers
/Users/me/Application Scripts/com.apple.iWork.Numbers.NumbersNotificationServiceExtension

❯ find "$HOME/Library/Application Scripts" -iname "*Numbers*" -maxdepth 1 -prune
/Users/me/Library/Application Scripts/com.apple.iWork.Numbers
/Users/me/Library/Application Scripts/com.apple.iWork.Numbers.NumbersNotificationServiceExtension

❯ echo $(getconf DARWIN_USER_CACHE_DIR | sed "s/\/$//")
/var/folders/2_/860qd2ks5hg6hp26wg2tmtn40000gn/C

❯ echo $(getconf DARWIN_USER_TEMP_DIR | sed "s/\/$//")
/var/folders/2_/860qd2ks5hg6hp26wg2tmtn40000gn/T

このスクリプトもそうですが、すべてのこのへんの記事の元は以下であることが多いようです。

👉 privacy-guides/how-to-clean-uninstall-macos-apps-using-appcleaner-open-source-alternative/app-cleaner.sh at master · sunknudsen/privacy-guides hatena-bookmark

ということで、アプリに関連するファイルの位置は以下だと言えそうです。


locations=(
  "$HOME/Library"
  "$HOME/Library/Application Scripts"
  "$HOME/Library/Application Support"
  "$HOME/Library/Application Support/CrashReporter"
  "$HOME/Library/Containers"
  "$HOME/Library/Caches"
  "$HOME/Library/HTTPStorages"
  "$HOME/Library/Group Containers"
  "$HOME/Library/Internet Plug-Ins"
  "$HOME/Library/LaunchAgents"
  "$HOME/Library/Logs"
  "$HOME/Library/Preferences"
  "$HOME/Library/Preferences/ByHost"
  "$HOME/Library/Saved Application State"
  "$HOME/Library/WebKit"
  "/Library"
  "/Library/Application Support"
  "/Library/Application Support/CrashReporter"
  "/Library/Caches"
  "/Library/Extensions"
  "/Library/Internet Plug-Ins"
  "/Library/LaunchAgents"
  "/Library/LaunchDaemons"
  "/Library/Logs"
  "/Library/Preferences"
  "/Library/PrivilegedHelperTools"
  "/private/var/db/receipts"
  "/usr/local/bin"
  "/usr/local/etc"
  "/usr/local/opt"
  "/usr/local/sbin"
  "/usr/local/share"
  "/usr/local/var"
  $(getconf DARWIN_USER_CACHE_DIR | sed "s/\/$//")
  $(getconf DARWIN_USER_TEMP_DIR | sed "s/\/$//")
)

アプリを特定するのは、上記スクリプトの

「basename」「CFBundleIdentifier」

のようです。

 

🤔 おまけ

SwiftUI で書かれたアンインストールアプリがありました。

👉 Pearcleaner - Product Information, Latest Updates, and Reviews 2024 | Product Hunt hatena-bookmark

現在絶賛公開中の模様。

オープンソースです。

👉 alienator88/Pearcleaner: A free, source-available and fair-code licensed mac app cleaner hatena-bookmark


【macOS】公証つけて配布する方法 - 2024年版

調べながらやったが、


Product

  ↓

Archive

  ↓

Distrubute App

と進んでいくが、

「Developer ID」

の選択肢が表示されない。

 

🧑🏻‍💻 iOS から macOS に切り替える

ここですか。

Xcode 上部中央で切り替える。

これで、流れとしては、


Product

  ↓

Archive

  ↓

Distrubute App

または、Custom 経由で、


Product

  ↓

Archive

  ↓

Distrubute App

  ↓

Custom


というかんじで、

以前の「Developer ID」での処理は 「Direct Distribution」からいける。

選択肢のラベルが変わった感じか。

あとは Export すればいい。


👉 Notarizing macOS software before distribution | Apple Developer Documentation hatena-bookmark