【SwiftUI】iOS と macOS で互換したいコードの一つの解法

なるほどこうするのか。

iOS ↔ macOS と切り替えると、

ビルドで落ちて萎えるときの

一つの何かのきっかけに。

最新の SwiftUI ならシンプルです。


import SwiftUI

public extension Color {

    #if os(macOS)
    static let background = Color(NSColor.windowBackgroundColor)
    static let secondaryBackground = Color(NSColor.underPageBackgroundColor)
    static let tertiaryBackground = Color(NSColor.controlBackgroundColor)
    #else
    static let background = Color(UIColor.systemBackground)
    static let secondaryBackground = Color(UIColor.secondarySystemBackground)
    static let tertiaryBackground = Color(UIColor.tertiarySystemBackground)
    #endif
}

👉 ios - SwiftUI: Get the Dynamic Background Color (Dark Mode or Light Mode) - Stack Overflow hatena-bookmark

マルチなプラットフォーム作成時には、押さえておきたい記述です。



【SwiftUI】View を ドラッグ して移動 する

View をドラッグして動かします。


struct TestDragGesture: View {
  @State private var location = CGPoint(x: 150, y: 150)

  var body: some View {
    ZStack {
      VStack(alignment: .trailing) {
        Text("location.x: \(location.x)")
        Text("location.y: \(location.y)")
      }
      .monospaced()

      Circle()
        .fill(.red.opacity(0.5))
        .frame(width: 100)
        .position(location)
        .gesture(
          DragGesture()
            .onChanged { value in
              location = value.location
            }
        )
    }
  }
}

ドラッグ位置が変化するたびに View の位置を更新できます。

var location: CGPoint
The location of the drag gesture’s current event.

👉 DragGesture.Value | Apple Developer Documentation hatena-bookmark

Circle の position は Viewの中心

であることに対して

DragGesture().onChanged() で取得したドラッグ位置

とずれがあるため、

タップ位置を動かした瞬間に中心位置にジャンプしてしまいます。

なんか違和感がありますね !

 

👩🏼‍💻 startLocation と translation

計算方法を変えます。

以下を使います。

var startLocation: CGPoint
The location of the drag gesture’s first event.

var translation: CGSize
The total translation from the start of the drag gesture to the current event of the drag gesture.

👉 DragGesture.Value | Apple Developer Documentation hatena-bookmark


Circle()
  .fill(.red.opacity(0.5))
  .frame(width: 100)
  .position(location)
  .gesture(
    DragGesture()
      .onChanged { value in
        var newLocation = startLocation ?? location
        newLocation.x += value.translation.width
        newLocation.y += value.translation.height
        location = newLocation
      }
      .updating($startLocation) { _, startLocation, _ in
        startLocation = startLocation ?? location
      }
   )

Circle の中心位置に対して、ドラッグの移動量を増減することで、なめらかに違和感なく動くようになりました !



簡単なコードに見えますが、updating を使った位置情報の入れ替えとか、よくみると深い。

👉 updating(_:body:) | Apple Developer Documentation hatena-bookmark
👉 Move your view around with Drag Gesture in SwiftUI | Sarunw hatena-bookmark

しかし、なんか見通しが悪い。

@GestureState と updating() は使いづらい。

 

👩🏼‍💻 まとめ

これでいきます。



キモは、

「ドラッグの移動量を View のポジションに適用」

「ドラッグ開始時のドラッグ位置とView中心のズレ」



【SwiftUI】.contentTransition(.symbolEffect(.replace))



これよく見かけるのでやってみます。


.contentTransition(.symbolEffect(.replace))

まず、Button の Label のアイコンを、

ある値を見ながら切り替えます。

よくあるパターンなので想像はつくでしょう。


@State private var value = false


Button {
  value.toggle()
} label: {
  Label("Favorite", systemImage: value ? "heart.fill" : "heart")
}

次に .symbolVariant() を使います。

挙動は同じだと思われます。


Button {
  value.toggle()
} label: {
  Label("Favorite", systemImage: "heart")
    .symbolVariant(value ? .fill : .none)
}

ここで、

.contentTransition(.symbolEffect(.replace))

付けます。


Button {
  value.toggle()
} label: {
  Label("Favorite", systemImage: "heart")
    .symbolVariant(value ? .fill : .none)
    .contentTransition(.symbolEffect(.replace))
}

Apple 公式サンプルコードでは、

以下のようにさらに細かく調整されていました。


Button {
  value.toggle()
} label: {
  Label("Favorite", systemImage: "heart")
    .symbolVariant(value ? .fill : .none)
    .contentTransition(
      .symbolEffect(value ? .replace.upUp : .replace.downUp)
    )
}

以上4つを並べて確認します。

微妙にエフェクト具合が違うけど、

どれがいいとはいいづらい。

👉 【SwiftUI】市松模様を背景にする - Checkered Pattern Background 🏁 hatena-bookmark

 

🧑🏻‍💻 まとめ

タイトルどおりの


.contentTransition(.symbolEffect(.replace))

ぐらいがシンプルな記述のわりに見栄えが良いように思います。

みんなが良く使ってる意味が分かりました !


【SwiftUI】#Preview with @Binding arguments

#Preview@Binding の引数を持つ View をどう書くか。

サンプルとして以前のコードを使います。


.constant() を使うか、@State + return を使うか。


#Preview(".constant(\"\")") {
  TestSearchTextField(text: .constant(""))
    .frame(width: 300)
}

#Preview(".constant(\"dog\")") {
  TestSearchTextField(text: .constant("dog"))
    .frame(width: 300)
}

#Preview("return") {
  @State var text = "dog"
  return TestSearchTextField(text: $text)
    .frame(width: 300)
}

View パーツの確認をすばやく #Preview で確認できるようになりました !


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

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

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

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