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 中文版
iOS 18 新API:使用 Navigation Transition 創建 Hero 動畫式過場
SwiftUI 框架

iOS 18 新API:使用 Navigation Transition 創建 Hero 動畫式過場

Apple 的工程師可能早已認識到,許多 iOS 開發者都希望能夠重現 App Store 應用程式中的優雅 Hero 動畫。由於從頭實現這種動畫通常需要耗費大量時間與精力,Apple 在 iOS 18 SDK 中納入了這項功能。 透過這次更新,你現在只需少量的程式碼就能在自己的應用程式中實現類似的動畫過渡效果。這項重大改進讓開發者能夠創造出更具視覺吸引力且流暢的過渡效果,
如何使用 Vision APIs 從圖像中辨識文字
AI

如何使用 Vision APIs 從圖像中辨識文字

Vision 框架長期以來一直包含文字識別功能。我們已經有詳細的教程,向你展示如何使用 Vision 框架掃描圖像並執行文字識別。之前,我們使用了 VNImageRequestHandler 和 VNRecognizeTextRequest 來從圖像中提取文字。 多年來,Vision 框架已經顯著演變。在 iOS 18 中,Vision
iOS 18更新:SwiftUI 新功能介紹
SwiftUI 框架

iOS 18更新:SwiftUI 新功能介紹

SwiftUI的技術不斷演進,每次更新都讓 iOS 應用程式開發變得更加便捷。隨著 iOS 18 Beta 的推出,SwiftUI 引入了多個令人興奮的新功能,使開發者僅需幾行程式碼即可實現出色的效果。 本教學文章旨在探索這個版本中的幾項主要改進,幫助你了解如何運用這些新功能。 浮動標籤列 (Floating Tab Bar)SwiftUI中的標籤視圖(Tab
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。