SwiftUI 框架

利用 SwiftUI 構建一個輪盤選擇器 (Wheel Picker)

SwiftUI 以一種創新且極度簡單的方式,讓開發者以聲明式語法 (Declarative Syntax) 開發 UI。在這篇文章中,我會帶大家在 SwiftUI 中,構建一個輪盤選擇器 (Wheel Picker),並獲取使用者的滑動動作方向,讓我們的 App 更加豐富。
SwiftUI Wheel Picker
SwiftUI Wheel Picker
In: SwiftUI 框架

本篇原文(標題:Wheel Picker View in SwiftUI)刊登於作者 Medium,由 Sarah 所著,並授權翻譯及轉載。

在這篇文章中,讓我們一起在 SwiftUI 構建一個輪盤選擇器 (Wheel Picker),並獲取使用者的滑動動作方向。

首先,讓我們從輪盤要顯示的數據開始。

如果我們想用輪盤來選擇顏色,我們就可以儲存顏色的數值。

如果我們想做一個選單,把圖片放在輪盤中央,我們就可以添加圖片變數。

在這篇文章的範例中,我們會在輪盤的外圍顯示數字,供使用者選擇。

struct myVal : Equatable {
    let id = UUID()
    let val : String
}

使用者需要向左或向右滑動輪盤,來選擇數字。讓我們建立一個列舉 (enum),來設定使用者可以滑動的方向。

enum Direction {
    case left
    case right
}

接下來,我們要建立一個視圖,並在視圖中添加以下變數:

  • radius:這是輪盤的半徑。從父視圖中取得了輪盤框架的大小後,這個在 Appearance 的數值就會被更改。
  • direction:儲存使用者滑動的方向。
  • choosenIndex:儲存使用者從輪盤選擇的數值。
  • degree:輪盤轉動的角度和其內部視圖 (internal view)。
  • array:這是我們剛剛創建的結構 (struct) myVal 的陣列 (array)。這個陣列是用來構建輪盤內的內部視圖。
  • circleSize:輪盤的寬度和高度。
struct WheelView: View {
    // Circle Radius
    @State var radius : Double = 150
    // Direction of swipe
    @State var direction = Direction.left
    // index of the number at the bottom of the circle
    @State var chosenIndex = 0
    // degree of circle and hue
    @Binding var degree : Double
    
     let array : [myVal]
     let circleSize : Double
     
     var body: some View {
      // BODY
     }
    
    }

讓我們建立一個函式,來按使用者滑動的方向,轉動輪盤到下一個數值。

用 360 除以陣列中數值的數量,來計算新的角度。我們也需要一直追蹤使用者選擇的數字。

    func moveWheel() {
        withAnimation(.spring()) {
            if direction == .left {
                degree += Double(360/array.count)
                if chosenIndex == 0 {
                    chosenIndex = array.count-1
                } else {
                    chosenIndex -= 1
                }
            } else {
                degree -= Double(360/array.count)
                if chosenIndex == array.count-1 {
                    chosenIndex = 0
                } else {
                    chosenIndex += 1
                }
            }
        }
    }

我們也會在 body 的開頭設置一些變數。首先,我們需要知道陣列中每個數值之間的間距 (spacing) /角度 (angle)。然後,我們可以使用 onEnded() 修飾符創建拖曳手勢 (drag gesture)。

onEnded 中,讓我們把滑動動作的起始位置 x 與動作結束的位置進行比較。如果起始值大於結束值,就代表使用者向左滑動,反之亦然。在 onEnded() 的最後,讓我們調用 moveWheel 函式來轉動輪盤。

    var body: some View {
        ZStack {
            let anglePerCount = Double.pi * 2.0 / Double(array.count)
            let drag = DragGesture()
                .onEnded { value in
                    if value.startLocation.x > value.location.x + 10 {
                        direction = .left
                    } else if value.startLocation.x < value.location.x - 10 {
                        direction = .right
                    }
                    moveWheel()
                }
        }
        .frame(width: circleSize, height: circleSize)
    }

接下來,我們把 Circle() 嵌入到 ZStack 來創建輪盤,然後 loop through 數值的陣列,來把數值添加到輪盤中。

然後,讓我們計算陣列來每個數值的角度、x offset、和 y offset。

在這個範例中,我們會使用 Text(),而數值就是從 0 到 10。

在選擇的視圖中,添加 .rotationEffect()、以及綁定到父視圖的角度。內部視圖會沿著與輪盤本身相反的方向移動。

在 loop 中,讓我們利用 x offset 和 y offset,來偏移 x 和 y 數值。

最後,在輪盤內突出顯示被選取的數值。我們在範例中會用不同的字體,來突出顯示被選取的數值。

在輪盤的 ZStack 中,添加 rotationEffect()、以及綁定到父視圖的角度。

接著,讓我們利用 .gesture() 修飾符,來把拖曳手勢添加到 Stack 中。

    var body: some View {
        ZStack {
            let anglePerCount = Double.pi * 2.0 / Double(array.count)
            let drag = DragGesture()
                .onEnded { value in
                    if value.startLocation.x > value.location.x + 10 {
                        direction = .left
                    } else if value.startLocation.x < value.location.x - 10 {
                        direction = .right
                    }
                    moveWheel()
                }
            // MARK: WHEEL STACK - BEGINNING
            ZStack {
                Circle().fill(EllipticalGradient(colors: [.orange,.yellow]))
                    .hueRotation(Angle(degrees: degree))

                ForEach(0 ..< array.count) { index in
                    let angle = Double(index) * anglePerCount
                    let xOffset = CGFloat(radius * cos(angle))
                    let yOffset = CGFloat(radius * sin(angle))
                    Text("\(array[index].val)")
                        .rotationEffect(Angle(degrees: -degree))
                        .offset(x: xOffset, y: yOffset )
                        .font(Font.system(chosenIndex == index ? .title : .body, design: .monospaced))
                }
            }
            .rotationEffect(Angle(degrees: degree))
            .gesture(drag)
            .onAppear() {
                radius = circleSize/2 - 30 // 30 is for padding
            }
            // MARK: WHEEL STACK - END
        }
        .frame(width: circleSize, height: circleSize)
    }

WheelView

以下是完整的程式碼:

struct WheelView: View {
    // Circle Radius
    @State var radius : Double = 150
    // Direction of swipe
    @State var direction = Direction.left
    // index of the number at the bottom of the circle
    @State var chosenIndex = 0
    // degree of circle and hue
    @Binding var degree : Double
//    @State var degree = 90.0

    let array : [myVal]
    let circleSize : Double

    
    func moveWheel() {
        withAnimation(.spring()) {
            if direction == .left {
                degree += Double(360/array.count)
                if chosenIndex == 0 {
                    chosenIndex = array.count-1
                } else {
                    chosenIndex -= 1
                }
            } else {
                degree -= Double(360/array.count)
                if chosenIndex == array.count-1 {
                    chosenIndex = 0
                } else {
                    chosenIndex += 1
                }
            }
        }
    }
    
    var body: some View {
        ZStack {
            let anglePerCount = Double.pi * 2.0 / Double(array.count)
            let drag = DragGesture()
                .onEnded { value in
                    if value.startLocation.x > value.location.x + 10 {
                        direction = .left
                    } else if value.startLocation.x < value.location.x - 10 {
                        direction = .right
                    }
                    moveWheel()
                }
            // MARK: WHEEL STACK - BEGINNING
            ZStack {
                Circle().fill(EllipticalGradient(colors: [.orange,.yellow]))
                    .hueRotation(Angle(degrees: degree))

                ForEach(0 ..< array.count) { index in
                    let angle = Double(index) * anglePerCount
                    let xOffset = CGFloat(radius * cos(angle))
                    let yOffset = CGFloat(radius * sin(angle))
                    Text("\(array[index].val)")
                        .rotationEffect(Angle(degrees: -degree))
                        .offset(x: xOffset, y: yOffset )
                        .font(Font.system(choosenIndex == index ? .title : .body, design: .monospaced))
                }
            }
            .rotationEffect(Angle(degrees: degree))
            .gesture(drag)
            .onAppear() {
                radius = circleSize/2 - 30 // 30 is for padding
            }
            // MARK: WHEEL STACK - END
        }
        .frame(width: circleSize, height: circleSize)
    }
}

父視圖

在父視圖中,添加我們剛剛創建好的 WheelView,並將角度、輪盤陣列、和圓形的大小傳遞給輪盤。然後,讓我們 offset 修飾符將輪盤設置在螢幕頂部。

struct ContentView: View {
    @State var degree = 90.0
    let array : [myVal] =  [myVal(val: "0"),
                            myVal(val: "1"),
                            myVal(val: "2"),
                            myVal(val: "3"),
                            myVal(val: "4"),
                            myVal(val: "5"),
                            myVal(val: "6"),
                            myVal(val: "8"),
                            myVal(val: "9"),
                            myVal(val: "10")]

    var body: some View {
        ZStack (alignment: .center){
            Color.orange.opacity(0.4).ignoresSafeArea()
                .hueRotation(Angle(degrees: degree))
            
            WheelView(degree: $degree, array: array, circleSize: 400)
                .offset(y: -350)
                .shadow(color: .white, radius: 4, x: 0, y: 0)
        }
    }
}
swiftui-wheel-picker

謝謝你的閱讀!

本篇原文(標題:Wheel Picker View in SwiftUI)刊登於作者 Medium,由 Sarah 所著,並授權翻譯及轉載。

作者簡介:Sarah,App 開發者,熱愛科技。

譯者簡介:Kelly Chan-AppCoda 編輯小姐。

作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
更多來自 AppCoda 中文版
如何在 SwiftUI App 中開發 Live Activities
SwiftUI 框架

如何在 SwiftUI App 中開發 Live Activities

Live Activities 首次於 iOS 16 推出,是 Apple 最令人興奮的更新之一,能讓 App 與使用者在即時互動上更有連結。它不再需要使用者不斷打開 App,Live Activities 可以讓資訊直接顯示在鎖定畫面和 Dynamic Island 上。
使用 Tool Calling 強化 Foundation Models 功能
AI

使用 Tool Calling 強化 Foundation Models 功能

在前幾篇教學中,我們介紹了 Foundation Models 在 iOS 26 中的運作方式,以及如何使用這個全新框架打造具備 AI 功能的應用。我們也介紹了 @Generable 巨集,它能輕鬆地將模型回應轉換為結構化的 Swift 類型。 現在,在這個 Foundation
活用 Foundation Models 的 @Generable 與 @Guide 製作測驗 App
AI

活用 Foundation Models 的 @Generable 與 @Guide 製作測驗 App

在前一篇教學中,我們介紹了 Foundation Models 框架,並示範了如何用它來進行基本的內容生成。那個過程相當簡單——你提供一個提示詞(prompt),等幾秒鐘,就能獲得自然語言的回應。在我們的範例中,我們建立了一個簡單的問答 App,讓使用者可以提問,App 則直接顯示生成的文字。 但如果回應變得更複雜——你需要把非結構化文字轉換為結構化的物件呢? 舉例來說,
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。