成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

使用 SwiftUI 創建一個靈活的選擇器

開發 前端
這篇文章介紹了如何使用 SwiftUI 構建一個靈活的選擇器(FlexiblePicker),用于選擇多個選項。首先創建了一個 ??Selectable?? 協議,使得選擇的選項對象需要實現 ??DisplayedName?? 和IsSelected?? 屬性。

前言

最近,在我正在開發一個在 Dribbble 上找到的設計的 SwiftUI 實現時,我想到了一個點子,可以通過一些酷炫的篩選器擴展該項目以縮小結果列表。

我決定篩選視圖將由兩個獨立的篩選選項組成,兩者都有一些可選項可供選擇。但然后我遇到了一個問題。在使用 UIKit 時,我總是將這種類型的視圖實現為具有特定 UICollectionViewFlowLayout 的 UICollectionView。但在 SwiftUI 中該如何實現呢?

讓我們來看看使用 SwiftUI 創建靈活選擇器的實現!

可選擇協議

選擇器的最重要部分是,我們可以通過該視圖組件選擇一些所需的選項。因此,首先創建了一個 Selectable 協議。

所有符合該協議的對象必須實現兩個屬性:displayedName(在選擇器中顯示的名稱)和 isSelected(一個布爾值,指示特定選項是否已選擇)。

此外,為了能夠通過映射字符串值數組創建 Selectable 對象,實現 Selectable 的對象必須提供帶 displayedName 作為參數的自定義初始化。

Identifiable 和 Hashable 協議確保我們可以輕松創建具有 ForEach 循環的 SwiftUI 視圖。此外,符合 Selectable 協議的所有對象都將實現存儲 UUID 值的常量 id。

我會故意省略符合 Selectable 協議的對象的實現,因為我認為這是顯而易見的。核心代碼如下:

protocol Selectable: Identifiable, Hashable {
    var displayedName: String { get }
    var isSelected: Bool { get set }
    
    init(displayedName: String)
}

自定義化

我的目標不僅是創建靈活的選擇器的實現,還要盡量使其可自定義。

因此,將使用符合 Selectable 協議的泛型類型 T 創建 FlexiblePicker。這樣,以后更容易重用該組件,因為它將是獨立于類型的。

在實現選擇器本身之前,我列出了所有可自定義屬性。接下來,創建了用于計算特定字符串值的寬度和高度的字符串擴展。由于我的實現允許更改字體大小和權重,因此先前提到的兩個擴展都以由靈活選擇器使用的 UIFont 作為參數。

extension String {
    func getWidth(with font: UIFont) -> CGFloat {
        let fontAttributes = [NSAttributedString.Key.font: font]
        let size = self.size(withAttributes: fontAttributes)
        return size.width
    }
    
    func getHeight(with font: UIFont) -> CGFloat {
        let fontAttributes = [NSAttributedString.Key.font: font]
        let size = self.size(withAttributes: fontAttributes)
        return size.height
    }
}

由于我的字符串擴展用于計算給定字符串的大小,因此需要將所有 UIFont 權重轉換為 SwiftUI 等效項。

這就是為什么我引入了一個 FontWeight 枚舉,其中包含以 UIFont 權重命名的所有可能情況。

此外,該枚舉有兩個屬性,一個返回 UIFont 權重,另一個返回 SwiftUI Font 權重。通過這種方式,我們只需向 FlexiblePicker 提供 FontWeight 枚舉的特定情況。

enum FontWeight {
    case light
    // the rest of possible cases
    
    var swiftUIFontWeight: Font.Weight {
        switch self {
        case .light:            return .light
        // switching through the rest of possible cases 
        }
    }
    
    var uiFontWeight: UIFont.Weight {
        switch self {
        case .light:            return .light
        // switching through the rest of possible cases 
        }
    }
}

FlexiblePicker 邏輯

之后,我終于準備好開始編寫 FlexiblePicker 的實現了。

首先,我需要一個函數來計算并返回輸入數據的所有寬度。我通過將所有輸入值映射到元組中,其中包含輸入值和自身的寬度來完成。

在映射中,我使用 reduce 函數來總結與給定輸入值相關聯的所有寬度(文本寬度、邊框寬度、文本填充和間距)。

private func calculateWidths(for data: [T]) -> [(value: T, width: CGFloat)] {
    return data.map { selectableType -> (T, CGFloat) in
        let font = UIFont.systemFont(ofSize: fontSize, weight: fontWeight.uiFontWeight)
        let textWidth = selectableType.displayedName.getWidth(with: font)
        let width = [textPadding, textPadding, borderWidth, borderWidth, spacing]
            .reduce(textWidth, +)
        return (selectableType, width)
    }
}

現在,計算寬度的函數準備好了,我們可以遍歷所有輸入數據并將它們分成單獨的數組。每個數組包含能夠適應同一 HStack 中的項目的項目。邏輯很簡單。我們有兩個數組:

  • singleLineResult 數組——負責存儲適合特定行的項目。
  • allLinesResult 數組——負責存儲所有項目數組(每個數組都等同于一行項目)。

首先,我們檢查從 HStack 行寬中減去項寬的結果是否大于0。

如果滿足條件,我們將當前項附加到 singleLineResult 中,更新可用的 HStack 行寬,并繼續到下一個元素。

如果結果小于 0,這意味著我們無法將下一個元素放入給定行中,因此我們將 singleLineResult 附加到 allLinesResult 中,將 singleLineResult 設置為僅由當前元素組成的數組(不能適應上一行的元素),并通過減去當前項的寬度來更新 HStack 的行寬。

在遍歷所有元素之后,我們必須處理特定的邊緣情況。singleLineResult 可能不會為空,也不會附加到 allLinesResult 中——因為我們只在減去項目寬度的結果小于 0 時附加 singleLineResult。在這種情況下,我們必須檢查 singleLineResult 是否為空。如果為真,我們返回 allLinesResult,如果不為真,我們必須首先附加 singleLineResult,然后返回 allLinesResult。

private func divideDataIntoLines(lineWidth: CGFloat) -> [[T]] {
    let data = calculateWidths(for: inputData)
    var singleLineWidth = lineWidth
    var allLinesResult = [[T]]()
    var singleLineResult = [T]()
    var partialWidthResult: CGFloat = 0
    data.forEach { (selectableType, width) in
        partialWidthResult = singleLineWidth - width
        if partialWidthResult > 0 {
            singleLineResult.append(selectableType)
            singleLineWidth -= width
        } else {
            allLinesResult.append(singleLineResult)
            singleLineResult = [selectableType]
            singleLineWidth = lineWidth - width
        }
    }
    guard !singleLineResult.isEmpty else { return allLinesResult }
    allLinesResult.append(singleLineResult)
    return allLinesResult
}

最后但并非最不重要的是,我們必須計算 VStack 的高度,以使 SwiftUI 更容易解釋我們的視圖組件。VStack 的高度是根據兩個值計算的:

  • 輸入數據中任何項目的高度(類似于寬度的計算,通過使用 reduce 函數,總結與項目相關的所有高度)。
  • 將顯示在 VStack 中的行數。
private func calculateVStackHeight(width: CGFloat) -> CGFloat {
    let data = divideDataIntoLines(lineWidth: width)
    let font = UIFont.systemFont(ofSize: fontSize, weight: fontWeight.uiFontWeight)
    guard let textHeight = data.first?.first?.displayedName
            .getHeight(with: font) else { return 16 }
    let result = [textPadding, textPadding, borderWidth, borderWidth, spacing]
        .reduce(textHeight, +)
    return result * CGFloat(data.count)
}

將這兩個數字相乘的結果將是我們的 VStack 的高度。

FlexiblePicker 視圖

最后,當所有邏輯準備好后,我們需要實現一個視圖主體。如我之前所提到的,視圖將使用嵌套的 ForEach 循環創建。

需要記住的是,ForEach 循環要求迭代的集合中的每個元素必須符合 Identifiable 協議,或者應該具有唯一的標識符。

這就是為什么我將分隔行的結果映射到元組中,其中包含每行和 UUID 值。

由于如此,我可以向 ForEach 循環提供 id 參數。另一點需要記住的是,ForEach 循環期望獲得一些 View 作為返回值。

如果我們只插入另一個 ForEach 循環,我們將在視圖的適當功能性方面遇到問題,因為 ForEach 不是一種 View。

這就是為什么我首先將整個 ForEach 循環包裝在 HStack 中,然后再包裝在 Group 中,以確保編譯器可以正確解釋一切。

var body: some View {
    GeometryReader { geo in
        VStack(alignment: alignment, spacing: spacing) {
            ForEach(
              divideDataIntoLines(lineWidth: geo.size.width)
                  .map { (data: $0, id: UUID()) }, 
              id: \.id
            ) { dataArray in
                Group {
                    HStack(spacing: spacing) {
                        ForEach(dataArray.data, id: \.id) { data in
                            Button(action: { updateSelectedData(with: data)
                            }) {
                                Text(data.displayedName)
                                    .lineLimit(1)
                                    .foregroundColor(textColor)
                                    .font(.system(
                                        size: fontSize, 
                                        weight: fontWeight.swiftUIFontWeight
                                    ))
                                    .padding(textPadding)
                            }
                            .background(
                                data.isSelected
                                ? selectedColor.opacity(0.5)
                                : notSelectedColor.opacity(0.5)
                            )
                            .cornerRadius(10)
                            .disabled(!isSelectable)
                            .overlay(RoundedRectangle(cornerRadius: 10)
                                        .stroke(borderColor, lineWidth: borderWidth))
                        }
                    }
                }
            }
        }
        .frame(width: geo.size.width, height: calculateVStackHeight(width: geo.size.width))
    }
  }
}

幾乎所有都已經完成,我們只需添加一個函數來處理與按鈕的用戶交互。該函數只需切換特定數據的 isSelected 屬性。

private func updateSelectedData(with data: T) {
    guard let index = inputData.indices
      .first(where: { inputData[$0] == data }) else { return }
    inputData[index].isSelected.toggle()
}

其余的代碼很簡單,主要是配置所有屬性,如字體、顏色或邊框。此外,在 VStack 的底部,我們設置一個 frame,其中寬度取自 GeometryReader,高度則由先前創建的函數計算。

現在 FlexiblePicker 已經完成,可以使用了!

總結

這篇文章介紹了如何使用 SwiftUI 構建一個靈活的選擇器(FlexiblePicker),用于選擇多個選項。

首先創建了一個 Selectable 協議,使得選擇的選項對象需要實現 displayedName 和 isSelected 屬性。

然后,詳細介紹了實現該選擇器的邏輯,包括如何處理選項的布局、寬度和高度,以及如何處理用戶與按鈕的交互。

最后,提供了一個簡單的視圖實現,可以在 SwiftUI 中使用該選擇器。這個選擇器可用于創建各種交互式選擇界面。

責任編輯:姜華 來源: Swift社區
相關推薦

2011-08-08 13:15:35

QWrap

2023-03-15 09:00:43

SwiftUISlider

2017-02-09 18:01:22

Android圖片選擇器開發

2010-09-03 09:30:29

CSS選擇器

2022-11-07 08:42:50

iOS 16SwiftUI

2012-04-16 14:32:31

iOS選擇器代碼

2012-04-19 17:42:46

Titanium布局

2011-10-24 10:30:20

CSS

2013-03-11 10:30:56

CSSWeb

2009-07-16 11:02:33

Swing文件選擇器

2020-12-08 06:23:05

LockSupport線程工具

2025-04-29 10:28:25

2024-08-06 09:26:15

Zustand選擇器Action

2023-08-01 07:25:38

Expresso框架API

2023-12-01 08:31:20

HTML解析庫

2022-01-17 09:22:42

SwiftUI App Store開源

2021-08-16 12:13:02

SwiftUIList ArticleList

2023-09-06 18:37:45

CSS選擇器符號

2012-12-27 14:08:39

Android開發顏色選擇器

2011-11-28 13:42:55

Sencha Touc組件選擇器
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产羞羞视频在线观看 | 国产欧美一区二区三区久久手机版 | 拍戏被cao翻了h承欢 | 黄色成人在线观看 | 精品一区二区免费视频 | 久久久久国产精品午夜一区 | 国产成人精品一区二区三区四区 | 大乳boobs巨大吃奶挤奶 | 亚洲激情第一页 | 免费观看一级黄色录像 | 国产免费一区二区三区 | 国产精品一区二区免费看 | 久久伦理中文字幕 | 国产99热 | 奇米影视首页 | 色播视频在线观看 | 国精品一区二区 | 人人看人人爽 | 色五月激情五月 | 欧美日韩一区二区在线观看 | 欧美爱爱视频 | 91免费在线视频 | 久久小视频 | 日本激情视频网 | 国产视频精品在线观看 | 亚洲永久精品国产 | 久久久久精 | 日韩成人免费视频 | 国产精品乱码一区二三区小蝌蚪 | 在线观看亚洲专区 | 欧美久久国产精品 | 黄色一级免费看 | 日韩高清国产一区在线 | 亚洲国产情侣自拍 | 亚洲成人第一页 | 永久av| 日韩有码一区 | 成人久久18免费网站图片 | 国产激情在线 | 黑人精品欧美一区二区蜜桃 | 美女爽到呻吟久久久久 |