【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】CardView のような GroupBox は本当に便利なのか

こういうやつが簡単に作れる。

👉 GroupBox | Apple Developer Documentation hatena-bookmark


GroupBox("Today's Menu") {
  VStack(alignment: .leading) {
    Text("🍛 curry and rice")
    Text("🥗 green salad")
  }
}
.frame(width: 300)

 

🧑🏻‍💻 作ってみた

手作りと比較します。




VStack {
  HStack {
    Text("Today's Menu")
      .bold()
    Spacer()
  }
  VStack(alignment: .leading) {
    Text("🍛 curry and rice")
    Text("🥗 green salad")
  }
}
.padding()
.frame(width: 300)
.background(
  .background.secondary,
  in: .rect(cornerRadius: 8)
)

かなりコード量に差が出ますね。

 

🧑🏻‍💻 まとめ

便利ですね。

厳しいレイアウトの制限がない限り、

使う機会は多いかもしれません。

ちなみに、macOS で見てみると、

手書きのほうが意図通りとなりました。

少し残念。

👉 【SwiftUI】Default background Color in built-in View Component hatena-bookmark
👉 【SwiftUI】市松模様を背景にする - Checkered Pattern Background 🏁 hatena-bookmark
👉 【SwiftUI】四角の角を丸くする方法あれこれ hatena-bookmark
👉 【SwiftUI】 iOS / macOS の レイアウト記述を typealias で切り替える hatena-bookmark


【SwiftUI】TextField で debounce | Debouncing TextField

みんなは、どんな実装にしていますか。

入力文字を監視しつつの検索結果のリアルタイム反映的なやつを作るとき。

詰まる感じをどう解消していますか。

Debounce
only emit an item from an Observable if a particular timespan has passed without it emitting another item

👉 ReactiveX - Debounce operator hatena-bookmark

👉 debounce(for:scheduler:options:) | Apple Developer Documentation hatena-bookmark


import Combine

public final class DebounceObject: ObservableObject {
  @Published var text: String = ""
  @Published var debouncedText: String = ""
  private var bag = Set<AnyCancellable>()

  public init(dueTime: TimeInterval = 0.5) {
    $text
      .removeDuplicates()
      .debounce(for: .seconds(dueTime), scheduler: DispatchQueue.main)
      .sink(receiveValue: { [weak self] value in
        self?.debouncedText = value
      })
      .store(in: &bag)
  }
}


struct SearchView: View {
  @StateObject var debounceObject = DebounceObject()

  var body: some View {
    VStack {
      TextField(text: $debounceObject.text)
        .onChange(of: debounceObject.debouncedText) { text in
          // perform search
        }
    }
  }
}

👉 How to debounce TextField search in SwiftUI | Swift Discovery hatena-bookmark

combine てのもう時代遅れなのですか ?

今現在、本流の本筋の考え方のわかりやすいシンプルな記述を探します。

 

🤔 Tunous/DebouncedOnChange

直感的に使いやすい ViewModifier ライブラリです。

👉 Tunous/DebouncedOnChange: SwiftUI onChange View extension with debounce time hatena-bookmark

コード量が少ないので内部も把握しやすいです。

基本的な使い方は、0.25 秒をデバウンス時間として以下のように書けます。


TextField("Search-1", text: $text)
  .onChange(of: text, debounceTime: .seconds(0.25)) { newValue in
    debouncedText = newValue
  }

使ってみると分かるのは、View 内に @State として、

textdebouncedText

の2つが存在してしまうことが面倒です。

なので、それを避けるために、

DebounceTextField として View を外出しにして使います。


DebounceTextField2(titleKey: "Search-2", debouncedText: $debouncedText)


struct DebounceTextField2: View {
  var titleKey: String
  @Binding var debouncedText: String

  @State private var local = ""

  var body: some View {
    TextField(titleKey, text: $local)
      .onChange(of: local, debounceTime: .seconds(0.25)) { newValue in
        debouncedText = newValue
      }
  }
}

onChange() を使うのはどうなのか。

 

🤔 Binding をカスタムする

そもそも @State は、

@Binding な引数をもつコンポーネントに対して双方向に値が流れる

ということをきちんと頭に置きながら考えてみます。



struct DebounceTextField3: View {
  var titleKey: String
  @Binding var debouncedText: String

  @State private var delay: Task<Void, Never>?

  var body: some View {
    TextField(
      titleKey,
      text: Binding(
        get: { debouncedText },
        set: { newvalue in
          delay?.cancel()
          delay = Task {
            do {
              try await Task.sleep(for: .seconds(0.25))
            } catch { return }
            debouncedText = newvalue
          }
        }
      )
    )
  }
}

続いて、ちょっとしっかりしたライブラリを使ってみます。

👉 boraseoksoon/Throttler: One Line to throttle, debounce and delay: Say Goodbye to Reactive Programming such as RxSwift and Combine. hatena-bookmark


struct DebounceTextField4: View {
  var titleKey: String
  @Binding var debouncedText: String

  var body: some View {
    TextField(
      titleKey,
      text: Binding(
        get: { debouncedText },
        set: { newvalue in
          debounce(.seconds(0.25)) {
            debouncedText = newvalue
          }
        }
      )
    )
  }
}

 

🤔 まとめ

Custom Binding を使ったほうがスッキリします。