import SwiftUI import PhotosUI struct NineGridImagePicker: View { @Binding var images: [UIImage] var maxCount: Int = 9 var cornerRadius: CGFloat = 16 var spacing: CGFloat = 8 var horizontalPadding: CGFloat = 20 var onTapImage: (Int) -> Void = { _ in } @State private var pickerItems: [PhotosPickerItem] = [] var body: some View { GeometryReader { geometry in let columns = Array(repeating: GridItem(.flexible(), spacing: spacing), count: 3) let columnsCount: CGFloat = 3 let totalSpacing = spacing * (columnsCount - 1) let availableWidth = geometry.size.width - horizontalPadding * 2 let cellSide = (availableWidth - totalSpacing) / columnsCount LazyVGrid(columns: columns, spacing: spacing) { ForEach(0..= images.count && !(index == images.count && images.count < maxCount) { RoundedRectangle(cornerRadius: cornerRadius) .fill(Color.white.opacity(0.08)) } #endif if index < images.count { // 图片格子 ZStack(alignment: .topTrailing) { Image(uiImage: images[index]) .resizable() .scaledToFill() .frame(maxWidth: .infinity, maxHeight: .infinity) .contentShape(RoundedRectangle(cornerRadius: cornerRadius)) .onTapGesture { onTapImage(index) } Button { removeImage(at: index) } label: { Image(systemName: "xmark.circle.fill") .foregroundColor(.white) .background(Circle().fill(Color.black.opacity(0.4))) .font(.system(size: 16, weight: .bold)) } .padding(6) .buttonStyle(.plain) } } else if index == images.count && images.count < maxCount { // 添加按钮格子 PhotosPicker( selection: $pickerItems, maxSelectionCount: maxCount - images.count, selectionBehavior: .ordered, matching: .images ) { ZStack { RoundedRectangle(cornerRadius: cornerRadius) .fill(Color(hex: 0x1C143A)) Image(systemName: "plus") .foregroundColor(.white.opacity(0.6)) .font(.system(size: 32, weight: .semibold)) } } .onChange(of: pickerItems) { _, newItems in handlePickerItems(newItems) } } } .frame(height: cellSide) .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .contentShape(RoundedRectangle(cornerRadius: cornerRadius)) } } .padding(.horizontal, horizontalPadding) } .frame(height: gridHeight(forCount: max(images.count, 1))) } private func gridHeight(forCount count: Int) -> CGFloat { // 通过一个近似:用屏幕宽度估算高度以确保父布局正确测量。 // 每行 3 个,行数 = ceil(count / 3.0)。在 GeometryReader 中真实高度会覆盖此近似。 let screenWidth = UIScreen.main.bounds.width let columnsCount: CGFloat = 3 let totalSpacing = spacing * (columnsCount - 1) let availableWidth = screenWidth - horizontalPadding * 2 let side = (availableWidth - totalSpacing) / columnsCount let rows = ceil(CGFloat(count) / 3.0) let totalRowSpacing = spacing * max(rows - 1, 0) return side * rows + totalRowSpacing } private func handlePickerItems(_ items: [PhotosPickerItem]) { guard !items.isEmpty else { return } Task { @MainActor in var appended: [UIImage] = [] for item in items { if images.count + appended.count >= maxCount { break } if let data = try? await item.loadTransferable(type: Data.self), let image = UIImage(data: data) { appended.append(image) } } if !appended.isEmpty { images.append(contentsOf: appended) } pickerItems = [] } } private func removeImage(at index: Int) { guard images.indices.contains(index) else { return } images.remove(at: index) } }