使用 UIGestureRecognizerRepresentable 實現 SwiftUI 的 swipe 手勢 — iOS 18 新功能

開發 SwiftUI App 時,有時我們需要使用來自 UIKit 的元件,此時我們可用 UIViewControllerRepresentable & UIViewRepresentable 將 UIKit 的 controller & view 變成能在 SwiftUI 使用的 SwiftUI view。

令人開心的,iOS 18 SwiftUI 可以使用的 UIKit 元件更多了,現在我們可用 UIGestureRecognizerRepresentable 將 UIKit 的 gesture recognizer 變成能在 SwiftUI view 使用的手勢,實現的方法類似我們熟悉的 UIViewControllerRepresentable & UIViewRepresentable。

swipe 手勢是 iOS App 上常見的手勢,可惜目前 SwiftUI 尚未內建,現在有了 UIGestureRecognizerRepresentable,接下來彼得潘將以中華職棒六隊為例,將 UIKit 的 UISwipeGestureRecognizer 變成 SwiftUI 可偵測的手勢來切換下一個和上一個球隊。

以下我們定義遵從 UIGestureRecognizerRepresentable 的 SwipeGesture 時將示範三種不同的寫法。

  • SwipeGesture 的 property action 是 (UISwipeGestureRecognizer) -> Void。
  • SwipeGesture 的 property action 是 ( ) -> Void。
  • SwipeGesture 搭配 Binding 型別的 index。

定義遵從 UIGestureRecognizerRepresentable 的 SwipeGesture,property action 是 (UISwipeGestureRecognizer) -> Void

import SwiftUI

struct SwipeGesture: UIGestureRecognizerRepresentable {
let direction: UISwipeGestureRecognizer.Direction
let action: (UISwipeGestureRecognizer) -> Void

func makeUIGestureRecognizer(context: Context) -> UISwipeGestureRecognizer {
let gesture = UISwipeGestureRecognizer()
gesture.direction = direction
return gesture
}

func handleUIGestureRecognizerAction(_ recognizer: UISwipeGestureRecognizer, context: Context) {
action(recognizer)
}

}

說明。

  • property action。

偵測到 swipe 時要做的事。

  • function makeUIGestureRecognizer。

回傳的手勢代表 SwipeGesture 可偵測的手勢,我們設定手勢的方向後回傳 UISwipeGestureRecognizer。

  • function handleUIGestureRecognizerAction。

偵測到 swipe 手勢時會觸發 handleUIGestureRecognizerAction,我們在此呼叫 action。

在 SwiftUI 畫面使用 SwipeGesture

struct TeamsView: View {
let teams = [
"統一7-ELEVEn獅",
"中信兄弟",
"樂天桃猿",
"富邦悍將",
"味全龍",
"台鋼雄鷹"
]
@State private var index = 0

var body: some View {
Text("\(teams[index])")
.font(.largeTitle)
.gesture(
SwipeGesture(direction: .left, action: swipeAction)
)
.gesture(
SwipeGesture(direction: .right, action: swipeAction)
)
}

func swipeAction(_ gesture: UISwipeGestureRecognizer) {
if gesture.direction == .left {
index += 1
if index >= teams.count {
index = 0
}
} else if gesture.direction == .right {
index -= 1
if index < 0 {
index = teams.count - 1
}
}
}
}

由於要偵測左滑跟右滑的手勢,因此要呼叫兩次 gesture modifier,傳入左滑和右滑的 SwipeGesture,然後再用 function swipeAction 定義切換下一個和上一個球隊的程式。

學會第一種寫法後,以下我們再介紹另外兩種寫法。

SwipeGesture 的 property action 是 () -> Void

左滑的 SwipeGesture action 執行 TeamsView 的 next,右滑的 SwipeGesture action 執行 TeamsView 的 pre。

import SwiftUI

struct SwipeGesture: UIGestureRecognizerRepresentable {
let direction: UISwipeGestureRecognizer.Direction
let action: () -> Void

func makeUIGestureRecognizer(context: Context) -> UISwipeGestureRecognizer {
let gesture = UISwipeGestureRecognizer()
gesture.direction = direction
return gesture
}

func handleUIGestureRecognizerAction(_ recognizer: UISwipeGestureRecognizer, context: Context) {
action()
}
}
import SwiftUI

struct TeamsView: View {
let teams = [
"統一7-ELEVEn獅",
"中信兄弟",
"樂天桃猿",
"富邦悍將",
"味全龍",
"台鋼雄鷹"
]
@State private var index = 0

var body: some View {
Text("\(teams[index])")
.font(.largeTitle)
.gesture(
SwipeGesture(direction: .left, action: next)
)
.gesture(
SwipeGesture(direction: .right, action: pre)
)
}

func next() {
index += 1
if index >= teams.count {
index = 0
}
}

func pre() {
index -= 1
if index < 0 {
index = teams.count - 1
}
}
}

SwipeGesture 透過 Binding 修改 index

通常 swipe 觸發的功能會跟下一個 / 上一個有關,因此我們也可以從 SwipeGesture 修改 TeamsView 的 index 觸發畫面更新。除了 index,我們還新增了 property count,利用它儲存 TeamsView 傳來的成員數量。

import SwiftUI

struct SwipeGesture: UIGestureRecognizerRepresentable {
let direction: UISwipeGestureRecognizer.Direction
let count: Int
@Binding var index: Int

func makeUIGestureRecognizer(context: Context) -> UISwipeGestureRecognizer {
let gesture = UISwipeGestureRecognizer()
gesture.direction = direction
return gesture
}

func handleUIGestureRecognizerAction(_ recognizer: UISwipeGestureRecognizer, context: Context) {
if recognizer.direction == .left {
index += 1
if index >= count {
index = 0
}
} else if recognizer.direction == .right {
index -= 1
if index < 0 {
index = count - 1
}
}
}
}
import SwiftUI

struct TeamsView: View {
let teams = [
"統一7-ELEVEn獅",
"中信兄弟",
"樂天桃猿",
"富邦悍將",
"味全龍",
"台鋼雄鷹"
]
@State private var index = 0

var body: some View {
Text("\(teams[index])")
.font(.largeTitle)
.gesture(
SwipeGesture(direction: .left, count: teams.count, index: $index)
)
.gesture(
SwipeGesture(direction: .right, count: teams.count, index: $index)
)

}
}

參考連結

What’s new in SwiftUI 的範例。

struct VideoThumbnailScrubGesture: UIGestureRecognizerRepresentable {
@Binding var progress: Double

func makeUIGestureRecognizer(context: Context) -> VideoThumbnailScrubGestureRecognizer {
VideoThumbnailScrubGestureRecognizer()
}

func handleUIGestureRecognizerAction(
_ recognizer: VideoThumbnailScrubGestureRecognizer, context: Context
) {
progress = recognizer.progress
}
}

struct VideoThumbnailTile: View {
var body: some View {
VideoThumbnail()
.gesture(VideoThumbnailScrubGesture(progress: $progress))
}
}

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com