【SwiftUI】色を付ける記述、ややこしすぎませんか?

目指すのはこれ。

緑色の四角の中に、

青色の文字と

オレンジ色の白文字ボタンを置くだけ。

どう書いてますか、色付けの記述。


.foregroundColor()
.foregroundStyle()
.foreground()
.backgroundStyle()
.background() 
.tint() 
// ...

意外と整理できてない私。

 

🤔 四角

VStack を使うとして。


VStack {
}
.frame(width: 100, height: 200)
//.backgroundStyle(.green) // no effect
.background(.green)

シンプルな単色であれば .background()

 

🤔 文字


Text("Hello")
  //.foregroundColor(.blue) // deprecated
  .foregroundStyle(.blue)
  //.tint(.blue) // no effect

将来 deprecated となるのは避けます。

.foreground() は使えないし suggestion にも表示されない。

公式サンプルをみても、

Text の色付けはすべて .foregroundStyle() となっていました。

 

🤔 ボタン

意外と悩みます。

文字とボタン表面の色、

押したり離したりしたときの色の変化、

角の形、

と要素が多いです。

それぞれに使えそうな modifier も複数あります。

多く試して、使いやすものを定型として覚えておきたいです。

まずは、角が丸いボタンなので .buttonStyle(.bordered) を使います。


Button("World") {
}
//.foregroundColor(.white) // deprecated
.foregroundStyle(.white)
//.backgroundStyle(.orange) // no effect
.buttonStyle(.bordered)
//.background(.orange) // break border
//.backgroundStyle(.orange) // no effect
//.tint(.orange) // only border

色と形とエフェクトを同時に満たすことができませんでした。

.tint() でいけそうに思えましたが、

きっちりとしたオレンジが表面に付きません。

次は .buttonStyle(.borderedProminent) を使います。

「prominent」の意味は、

prominent 形
1.〔周囲より〕高くなった、突き出した
2. 人目を引く、目立つ、派手な
3. 有名な、著名な、優れた、卓越した

なので、「目立つ突き出たボタン」ということですね。


Button("World") {
}
.buttonStyle(.borderedProminent)
//.foregroundStyle(.white) // no need default color
//.background(.orange) // wrong background
//.backgroundStyle(.orange) // no effect
.tint(.orange)

これですね !

ボタンを押したときの変化もいいかんじです。

label や makebody などを使おうとも思いましたが、

不必要に長くなりそうなのでやめておきました。

 

🤔 まとめ

最終的にこう書けました。


VStack {
  Text("Hello")
    .foregroundStyle(.blue)
  Button("World") {
  }
  .buttonStyle(.borderedProminent)
  .tint(.orange)
}
.frame(width: 100, height: 100)
.background(.green)

色付けのざっくりシンプルイメージとして、


// テキストやシンボル
.foregroundStyle()

// ボタン表面
.buttonStyle(.borderedProminent)
.tint()

// 固定の背景色
.background()

ぐらいから書き始めるのが良さげに思います。

それで意図通りにいかないときはさらに考えるかんじで。

すべてを覚えられないので簡単なものから順番に。


【Swift】ログイン時に起動する - SMAppService


import Foundation
import ServiceManagement
import os.log

@available(macOS 13.0, *)
public enum LaunchOnLogin {
  public static var isEnabled: Bool {
    get { SMAppService.mainApp.status == .enabled }
    set {
      do {
        if newValue {
          if SMAppService.mainApp.status == .enabled {
            try? SMAppService.mainApp.unregister()
          }
          try SMAppService.mainApp.register()
        } else {
          try SMAppService.mainApp.unregister()
        }
      } catch {
        os_log("Failed to \(newValue ? "enable" : "disable") launch at login: \(error.localizedDescription)")
      }
    }
  }
}

👉 Rectangle/Rectangle/LaunchOnLogin.swift at 277cadc905d9ff92c7948a5c97794141f8d1277c · rxhanson/Rectangle hatena-bookmark


import AppKit
import ServiceManagement

@available(macOS 13.0, *)
class LoginItemManager {
  func loginItemIsEnabled() -> Bool {
    return SMAppService.mainApp.status == .enabled
  }

  func disableLoginItem() {
    try? SMAppService.mainApp.unregister()
  }

  func enableLoginItem() {
    try? SMAppService.mainApp.register()
  }
}

👉 phpmon/phpmon/Common/Helpers/LoginItemManager.swift at 4a3dee3c5023ef252e5dd48de4d2937087eb941b · nicoverbruggen/phpmon hatena-bookmark


import ServiceManagement

public extension SMAppService {
  var isEnabled: Bool {
    status == .enabled
  }

  func toggle() throws {
    isEnabled ? try unregister() : try register()
  }
}

👉 LunarBar/LunarBarMac/Modules/Sources/AppKitExtensions/SMAppService+Extension.swift at e7ae92a8fca7895d739686839afa450fd60663de · LunarBar-app/LunarBar hatena-bookmark

 

■ まとめ

以上を参考に、こう書きました。


@AppStorage("launchAtLogin") private var isOn = false

Toggle("Launch at login", isOn: $isOn)
  .onAppear {
    isOn = SMAppService.mainApp.status == .enabled
  }
  .onChange(of: isOn) {
    let app = SMAppService.mainApp
    try? isOn ? app.register() : app.unregister()
  }

その後、結局、

@AppStorage も @State も持たないようにしました。



登録状態は、


[Settings] - [General] - [Login Items]

でOS側設定画面からも管理できます。

しかし、双方向のバインディングはつらくないですか。

👉 SMAppService | Apple Developer Documentation hatena-bookmark


【SwiftUI】computed property vs func - computed property を上手に使いたい

なんとなく使っていた computed property と func。

気にはなっていたので。

以下の条件に当てはまる場合は、functionよりpropertyを使う方がよい。

- 例外を投げない
- 計算量が少ない(または初回実行時にキャッシュされる)。
- オブジェクトの状態が変化していない場合、何度呼び出しても同じ結果を返す。

(O(1)ではないcomputed propertyではその旨をドキュメント(コメント)に明記すること。 プロパティアクセスは一般的に計算コストが安価だと見なされるので、そうでない場合はその旨の明記が必要である。)

👉 [Swift] FunctionとComputed Propertyの使い分けの基準 #Swift - Qiita hatena-bookmark

* Top Hightlight
A property expresses an inherent quality of an instance, while a method performs an action.

プロパティはインスタンスの固有の品質を表現し、メソッドはアクションを実行します。

👉 Functions vs Computed property — What to use? | by Aaina jain | Swift India | Medium hatena-bookmark

うむむ、具体的にどう考えたらいいのか。

Apple 公式サンプルをさらう。


struct BackyardsSearchSuggestions: View {
  @Query private var backyards: [Backyard]
    
  var events: [BackyardVisitorEvent] {
    Set(backyards.compactMap(\.currentVisitorEvent))
      .sorted { ($0.backyard?.name ?? "") < ($1.backyard?.name ?? "") }
      .sorted { ($0.bird?.speciesName ?? "") < ($1.bird?.speciesName ?? "") }
    }
    
  var body: some View {
    ForEach(events) { event in

👉 sample-backyard-birds/Multiplatform/Backyards/BackyardsSearchSuggestions.swift at main · apple/sample-backyard-birds hatena-bookmark


struct BirdFoodPickerSheet: View {
  var backyard: Backyard
    
  @Query(sort: [.init(\BirdFood.priority, order: .reverse), .init(\BirdFood.name, comparator: .localizedStandard)])
  private var birdFood: [BirdFood]
    
  @Environment(\.dismiss) private var dismiss
  @State private var presentingBirdFoodShop = false
    
  private let metrics = BirdFoodStoreMetrics.birdFoodStore

  var premiumFood: [BirdFood] {
    birdFood.filter(\.isPremium)
  }
    
  var otherFood: [BirdFood] {
    birdFood.filter { !$0.isPremium }
  }

👉 sample-backyard-birds/Multiplatform/Backyards/BirdFoodPickerSheet.swift at main · apple/sample-backyard-birds hatena-bookmark


struct BirdsSearchSuggestions: View {
  @Query private var birds: [Bird]
    
  var speciesNames: [String] {
    Set(birds.map(\.speciesName)).sorted()
  }
    
  var body: some View {
    ForEach(speciesNames, id: \.self) { speciesName in

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


struct PlantsSearchSuggestions: View {
  @Query private var plants: [Plant]
    
  var speciesNames: [String] {
    Set(plants.map(\.speciesName)).sorted()
  }
    
  var body: some View {
    ForEach(speciesNames, id: \.self) { speciesName in

👉 sample-backyard-birds/Multiplatform/Plants/PlantsSearchSuggestions.swift at main · apple/sample-backyard-birds hatena-bookmark


struct BackyardBirdsPassShop: View {
  @Environment(\.dismiss) private var dismiss
  @Environment(\.passIDs.group) private var passGroupID
  @Environment(\.passStatus) private var passStatus
    
  private var showPremiumUpgrade: Bool {
    passStatus == .individual || passStatus == .family
  }

👉 sample-backyard-birds/Multiplatform/Shop/BackyardBirdsPassShop.swift at main · apple/sample-backyard-birds hatena-bookmark


struct BackyardsSearchSuggestions: View {
  @Query private var backyards: [Backyard]
    
  var events: [BackyardVisitorEvent] {
    Set(backyards.compactMap(\.currentVisitorEvent))
      .sorted { ($0.backyard?.name ?? "") < ($1.backyard?.name ?? "") }
      .sorted { ($0.bird?.speciesName ?? "") < ($1.bird?.speciesName ?? "") }
  }
    
  var body: some View {
    ForEach(events) { event in

👉 sample-backyard-birds/Multiplatform/Backyards/BackyardsSearchSuggestions.swift at main · apple/sample-backyard-birds hatena-bookmark

View 内の利用状況だけをみると、

ほぼ @Query の加工にしか使ってない。

逆に言えば

@Query の加工には Computed Property が使える。

ということは言えそう。

ちなみに、View 内に func はほとんど見当たらない。


【 Swift 】2次元配列と1次元配列の相互変換 🤔

こんな12年前の so の書き込み。


p.x = index / 3;
p.y = index % 3;

👉 c# - Convert 1D array index to 2D array index - Stack Overflow hatena-bookmark

さすが、先人先生。

やってみました。

長ったらしいですが考え方を思い出したいときのために。

 

🤔 配列をn個ずつに分割する

副産物としてこんなかんじに書けました。


extension Array {
  func chunked(by: Int) -> [[Element]] {
    return  (0 ..< (self.count / by)).map {
      Array(self[$0 * by ..< ($0 + 1) * by])
    }
  }
}

print(
  [1, 2, 3, 4, 5, 6].chunked(by: 3)
)
// [[1, 2, 3], [4, 5, 6]]

 

🤔 まとめ

配列の変換


let d2: [[String]] = [
  ["あ", "い", "う", "え", "お"],
  ["か", "き", "く", "け", "こ"],
  ["さ", "し", "す", "せ", "そ"]
]

// 2次元から1次元 *
let d1 = d2.flatMap { $0 }
print(d1)
// ["あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ", "さ", "し", "す", "せ", "そ"]

print(
  // 1次元から2次元 *
  (0 ..< d1.count / numCols).map {
    Array(d1[($0 * numCols) ..< ($0 + 1) * numCols])
  }
)
// [["あ", "い", "う", "え", "お"], ["か", "き", "く", "け", "こ"], ["さ", "し", "す", "せ", "そ"]]

extension 化する。


extension Array where Element: Collection { 
  func toD1() -> [Element.Element] { // nested
    return self.flatMap { $0 } 
  }
}

extension Array {
  func toD2(numCols: Int) -> [[Element]] {
    return (0 ..< self.count / numCols).map {
      Array(self[($0 * numCols) ..< ($0 + 1) * numCols])
    }
  }
}

1次元配列インデックスと2次元配列座標の関係


index = y * numCols + x

x = index % numCols
y = index / numCols

SwiftData の inMemory で実装してみたが遅かったので、@Observable クラスへ。

👉 【 SwiftUI 】 Pong Wars を SwiftUI に移植してみた hatena-bookmark

しかし、正直、勉強すればするほど謎が増えます !


👉 【Swift】2次元配列 で 転置行列 ( transpose matrix ) hatena-bookmark




Xcode の自動ビルドのせいで編集できないのだが - How to disable Automatically Refresh Canvas and background build compile

Xcode の自動ビルドというかコンパイルというか。

手動にしたいんですけど。

自動で裏でなんかしら動いて、

エディターを触るのに詰まる感じ。

ロジックをいじるときには非常にストレス。

 

😩 Settings → General → Show live issues OFF

良くわからない。

ON でも OFFでも何がどう変わるのか。

ググるとやたらヒットする。

👉 xcode 9 how to disable auto build … | Apple Developer Forums hatena-bookmark

 

😩 Editor → Canvas → Automatically Refresh Canvas OFF

You can turn off the auto-compile feature for previews/canvas'. When having a SwiftUI View open in the editor go to the Editor -> Canvas -> Automatically Refresh Canvas.

👉 Stop auto compile on Xcode preview - Stack Overflow hatena-bookmark

欲しかったのはこれかな?

ファイルを編集すると、Canvas 上に


「Preview paused 🔃」

と表示される。

🔃 を押さないと、自動でビルドは行われない。

これだわ !

 

😩 まとめ

Xcode が裏で動いてコード編集できないときは、


Editor

  ↓

Canvas 

  ↓

Automatically Refresh Canvas OFF

です。

リフレッシュするときのショートカットは、


⌥ (Option) + ⌘ (Command) + P

のようですです。

しかし、そもそも、

編集操作を妨げてまで

デフォルトで自動でリフレッシュする必要なくね?

初心者はつらい。いちいち詰まる。