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 使用 Preview Macro
SwiftUI 框架

如何在 SwiftUI 使用 Preview Macro

SwiftUI 中的預覽(Preview)功能允許開發人員在不運行應用程序於設備或模擬器上的情況下,實時查看App的外觀。這個功能對於那些希望快速迭代設計並確保一切看起來和運作如預期的開發人員來說非常有用。隨著 iOS 17 中引入了巨集(Macro)的功能,預覽功能變得更加強大和靈活,提供了更多的自定義和靈活性。在本教學中,我們將探索如何在 SwiftUI 中使用新的預覽巨集(Preview Macro)
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。