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 中文版
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。