swiftui 九宫格拖动排序

这适合没拖动到底部删除

import SwiftUI
import UniformTypeIdentifiers

struct ContentView: View {
    
    @StateObject var vm = VM()
    
    var body: some View {
        NavigationView{
            VStack{
                imageContents
                if(vm.draggingItem != nil){
                    bottomView
                }
                
            }
            .frame(maxWidth: .infinity,maxHeight: .infinity)
            .navigationBarTitle("可移动的九宫格", displayMode: .inline)
        }
        
    }
    
    var imageContents:some View{
        ScrollView {
            GeometryReader { geometry in
                // 创建LazyVGrid
                LazyVGrid(columns: vm.gridLayout, spacing: 12) {
                    ForEach(Array(vm.images.enumerated()), id: \.element) { index, item in
                        if(item.hasAdd){
                            itemView(item: item,width: (geometry.size.width - 24 - 32) / 3)
                                .onTapGesture {
                                    vm.imagePickerPresented()
                                }
                        }else{
                            itemView(item: item,width: (geometry.size.width - 24 - 32) / 3)
                                .onTapGesture {
                                    vm.imageBrowserPresented(index: index)
                                }
                                .onDrag {
                                    vm.draggingItem = item
                                    let provider = NSDelItemProvider(contentsOf: URL(string: "\(item.id)"))!
                                        provider.didEnd = {
                                            DispatchQueue.main.async {
                                                print("didEnd")
                                                vm.draggingItem = nil      // << here !!
                                            }
                                        }
                                    return provider
                                }
                                .onDrop(of: [.item], delegate: ImageMoveItemDropDelegate(item: item, vm: vm))
                        }
                    }
                }
                .padding(.horizontal, 16) // 增加水平方向的内边距
            }
        }
    }
    
    func itemView(item:ImageMoveItem,width:CGFloat) -> some View{
        ZStack{
            Image(uiImage:item.uiImage)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: item.hasAdd ? 32 : width,height: item.hasAdd ? 32 : width)
                .cornerRadius(8)
                .allowsHitTesting(false)
        }
        .frame(width:width,height: width)
        .cornerRadius(8)
        .overlay {
            if(item.hasAdd){
                RoundedRectangle(cornerRadius: 8, style: .continuous)
                    .stroke(.gray, lineWidth: 1)
            }
        }
        .contentShape(Rectangle())
    }
    
    var bottomView:some View{
        ZStack(alignment: .top, content: {
            Rectangle()
                .foregroundColor(.white)
                .frame(height: 36 + 53)
                .onDrop(of: [.item], delegate: ImageDelItemDropDelegate(vm: vm))
            Rectangle()
                .frame(height: 36)
                .foregroundColor(vm.isDraggingInDelView ? Color.red : Color.red.opacity(0.8))
                .padding(.top,53)
            Text("拖动到此处删除")
                .font(.system(size: 16).bold())
                .foregroundStyle(.white)
                .padding(10)
                .padding(.top,53)
        })
        .background(vm.isDraggingInDelView ? Color.red : Color.red.opacity(0.8))
        .background(ignoresSafeAreaEdges: .bottom)
    }
}

#Preview {
    ContentView()
}

//
//  VM.swift
//  testSign
//
//  Created by 1 on 2024/5/14.
//

import Foundation
import UIKit
import SwiftUI
import JFHeroBrowser

let keyWindow = UIApplication.shared.connectedScenes
                        .map({ $0 as? UIWindowScene })
                        .compactMap({ $0 })
                        .first?.windows.first

let myAppRootVC : UIViewController? = keyWindow?.rootViewController

let addImageMoveItem:ImageMoveItem = ImageMoveItem(uiImage: UIImage(named: "ic_imgadd") ?? UIImage(systemName: "plus")!,hasAdd: true)
let testImageMoveItem:ImageMoveItem = ImageMoveItem(uiImage: UIImage(systemName: "move.3d")!)

class VM: ObservableObject{
//    @Published var isImagePickerPresented = false
    
    // 定义网格布局
    let gridLayout: [GridItem] = Array(repeating: .init(.flexible()), count: 3)
    
    var photoPicker = QSPhotoPicker()
    @Published var images:[ImageMoveItem] = [
        testImageMoveItem,addImageMoveItem,
    ]
    var maxImages:Int = 9
    var getTrueImageNum:Int{
        get{
            var getTrueImageNum = 0
            for image in images {
                if(!image.hasAdd){
                    getTrueImageNum = getTrueImageNum+1
                }
            }
            return getTrueImageNum
        }
    }
    
    //拖拽
    @Published var draggingItem: ImageMoveItem?
    @Published var isDragging = false
    // 用于标识是否有项目被拖动到删除区域
    @Published var isDraggingInDelView: Bool = false

    init() {

    }

    func imageBrowserPresented(index:Int){
        var imageBrowser = QSImageBrowser()
        var list: [UIImage] = []
        for image in images {
            if(!image.hasAdd){
                list.append(image.uiImage)
            }
        }
        imageBrowser.uiimages = list
        imageBrowser.imageBrowserPresented(index: index)
    }
    
    func imagePickerPresented(){
        photoPicker.maxSelectCount = self.maxImages - self.getTrueImageNum
        photoPicker.selectImageBlock = { results in
            var tempsImages:[ImageMoveItem] = []
            for item in results{
                tempsImages.append(ImageMoveItem(uiImage: item))
            }
            let allImages = self.getTrueImageNum + tempsImages.count
            if(allImages == self.maxImages){//去除+
                self.images.removeLast()
                self.images.append(contentsOf: tempsImages)
            }else{
                self.images.insert(contentsOf: tempsImages, at: self.images.count-1)
            }
        }
        photoPicker.imagePickerPresented()
    }
}

//MARK: Drop Delegate
struct ImageMoveItemDropDelegate: DropDelegate {
    let item: ImageMoveItem
    var vm:VM
    /// Drop finished work
    func performDrop(info: DropInfo) -> Bool {
        vm.draggingItem = nil
        vm.isDragging = false
        return true
    }
    /// Moving style without "+" icon
    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }
    /// Object is dragged off of the onDrop view.
    func dropExited(info: DropInfo) {
        vm.isDragging = false
    }
    /// Object is dragged over the onDrop view.
    func dropEntered(info: DropInfo) {
        vm.isDragging = true
        guard let dragItem = vm.draggingItem, dragItem != item,
              let from = vm.images.firstIndex(of: dragItem),
              let to = vm.images.firstIndex(of: item) else {return}
        vm.images.move(fromOffsets: IndexSet(integer: from), toOffset: to > from ? to + 1 : to)
    }
}

class NSDelItemProvider: NSItemProvider {
    var didEnd: (() -> Void)?
    deinit {
        didEnd?()     // << here !!
    }
}
struct ImageDelItemDropDelegate: DropDelegate {
//    let item: ImageMoveItem
    var vm:VM
    /// Drop finished work
    func performDrop(info: DropInfo) -> Bool {
        if let dragItem = vm.draggingItem,
           let from = vm.images.firstIndex(of: dragItem) {
            vm.images.remove(at: from)
            if(!vm.images.contains(addImageMoveItem)){
                vm.images.append(addImageMoveItem)
            }
        }
        vm.draggingItem = nil
        vm.isDraggingInDelView = false
        return true
    }
    /// Moving style without "+" icon
    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }
    /// Object is dragged off of the onDrop view.
    func dropExited(info: DropInfo) {
//        vm.draggingItem = nil
        vm.isDraggingInDelView = false
    }
    /// Object is dragged over the onDrop view.
    func dropEntered(info: DropInfo) {
        vm.isDraggingInDelView = true
    }
}

struct ImageMoveItem: Identifiable, Hashable{//, Transferable
    var id = UUID()
    var uiImage: UIImage
    var hasAdd:Bool = false
//    static var transferRepresentation: some TransferRepresentation {
//        DataRepresentation(exportedContentType: .png) { value in
//            return value.uiImage.pngData()!
//        }
//    }
}

//struct ImageMoveItem: Identifiable, Hashable, Transferable, Codable {
//    var id = UUID()
//    var uiImage: UIImage
//    var hasAdd: Bool = false
//    enum CodingKeys: String, CodingKey {
//        case id
//        case uiImageData
//        case hasAdd
//    }
//    init(uiImage: UIImage, hasAdd: Bool = false) {
//        self.uiImage = uiImage
//        self.hasAdd = hasAdd
//    }
//    init(from decoder: Decoder) throws {
//        let container = try decoder.container(keyedBy: CodingKeys.self)
//        id = try container.decode(UUID.self, forKey: .id)
//        hasAdd = try container.decode(Bool.self, forKey: .hasAdd)
//        
//        let uiImageData = try container.decode(Data.self, forKey: .uiImageData)
//        guard let image = UIImage(data: uiImageData) else {
//            throw DecodingError.dataCorruptedError(forKey: .uiImageData,
//                                                   in: container,
//                                                   debugDescription: "UIImage data is not valid")
//        }
//        uiImage = image
//    }
//    func encode(to encoder: Encoder) throws {
//        var container = encoder.container(keyedBy: CodingKeys.self)
//        try container.encode(id, forKey: .id)
//        try container.encode(hasAdd, forKey: .hasAdd)
//        
//        guard let uiImageData = uiImage.pngData() else {
//            throw EncodingError.invalidValue(uiImage,
//                                             EncodingError.Context(codingPath: [],
//                                                                   debugDescription: "UIImage could not be encoded"))
//        }
//        try container.encode(uiImageData, forKey: .uiImageData)
//    }
//    static var transferRepresentation: some TransferRepresentation {
//        CodableRepresentation(contentType: .item)
//    }
//}

有拖动到底部删除

//
//  VM.swift
//  testSign
//
//  Created by 1 on 2024/5/14.
//

import Foundation
import UIKit
import SwiftUI
import JFHeroBrowser

let keyWindow = UIApplication.shared.connectedScenes
                        .map({ $0 as? UIWindowScene })
                        .compactMap({ $0 })
                        .first?.windows.first

let myAppRootVC : UIViewController? = keyWindow?.rootViewController

let addImageMoveItem:ImageMoveItem = ImageMoveItem(uiImage: UIImage(named: "ic_imgadd") ?? UIImage(systemName: "plus")!,hasAdd: true)
let testImageMoveItem:ImageMoveItem = ImageMoveItem(uiImage: UIImage(systemName: "move.3d")!)

class VM: ObservableObject{
//    @Published var isImagePickerPresented = false
    
    // 定义网格布局
    let gridLayout: [GridItem] = Array(repeating: .init(.flexible()), count: 3)
    
    var photoPicker = QSPhotoPicker()
    @Published var images:[ImageMoveItem] = [
        addImageMoveItem,
    ]
    var maxImages:Int = 9
    var getTrueImageNum:Int{
        get{
            var getTrueImageNum = 0
            for image in images {
                if(!image.hasAdd){
                    getTrueImageNum = getTrueImageNum+1
                }
            }
            return getTrueImageNum
        }
    }
    
    //拖拽
    @Published var draggingItem: ImageMoveItem?
    @Published var isDragging = false
    // 用于标识是否有项目被拖动到删除区域
    @Published var isDraggingInDelView: Bool = false

    //做个延迟处理
    @Published var isCanDraggingTarget: Bool = false
    
    init() {

    }

    func imageBrowserPresented(index:Int){
        let imageBrowser = QSImageBrowser()
        var list: [UIImage] = []
        for image in images {
            if(!image.hasAdd){
                list.append(image.uiImage)
            }
        }
        imageBrowser.uiimages = list
        imageBrowser.imageBrowserPresented(index: index)
    }
    
    func imagePickerPresented(){
        photoPicker.maxSelectCount = self.maxImages - self.getTrueImageNum
        photoPicker.selectImageBlock = { results in
            var tempsImages:[ImageMoveItem] = []
            for item in results{
                tempsImages.append(ImageMoveItem(uiImage: item))
            }
            let allImages = self.getTrueImageNum + tempsImages.count
            if(allImages == self.maxImages){//去除+
                self.images.removeLast()
                self.images.append(contentsOf: tempsImages)
            }else{
                self.images.insert(contentsOf: tempsImages, at: self.images.count-1)
            }
        }
        photoPicker.imagePickerPresented()
    }
}

//MARK: Drop Delegate
struct ImageMoveItemDropDelegate: DropDelegate {
    var item: ImageMoveItem
    var vm:VM
   
    /// Drop finished work
    func performDrop(info: DropInfo) -> Bool {
        vm.draggingItem = nil
        vm.isDragging = false
        vm.images = vm.images
        return true
    }
    /// Moving style without "+" icon
    func dropUpdated(info: DropInfo) -> DropProposal? {
        if(vm.isCanDraggingTarget){
            if let dragItem = vm.draggingItem, dragItem != item,
               let from = vm.images.firstIndex(of: dragItem),
               let to = vm.images.firstIndex(of: item){
                vm.images.move(fromOffsets: IndexSet(integer: from), toOffset: to > from ? to + 1 : to)
            }
        }
        return DropProposal(operation: .move)
    }
    /// Object is dragged off of the onDrop view.
    func dropExited(info: DropInfo) {
        vm.isDragging = false
    }
    /// Object is dragged over the onDrop view.
    func dropEntered(info: DropInfo) {
        vm.isDragging = true
        vm.isCanDraggingTarget = false
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5){
            vm.isCanDraggingTarget = true
        }
//        guard let dragItem = vm.draggingItem, dragItem != item,
//              let from = vm.images.firstIndex(of: dragItem),
//              let to = vm.images.firstIndex(of: item)
//        else {return}
//        vm.images.move(fromOffsets: IndexSet(integer: from), toOffset: to > from ? to + 1 : to)
    }
}

class NSDelItemProvider: NSItemProvider {
    var didEnd: (() -> Void)?
    deinit {
        didEnd?()     // << here !!
    }
}
struct ImageDelItemDropDelegate: DropDelegate {
//    let item: ImageMoveItem
    var vm:VM
    /// Drop finished work
    func performDrop(info: DropInfo) -> Bool {
        if let dragItem = vm.draggingItem,
           let from = vm.images.firstIndex(of: dragItem) {
            vm.images.remove(at: from)
            if(!vm.images.contains(addImageMoveItem)){
                vm.images.append(addImageMoveItem)
            }
        }
        vm.draggingItem = nil
        vm.isDraggingInDelView = false
        return true
    }
    /// Moving style without "+" icon
    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }
    /// Object is dragged off of the onDrop view.
    func dropExited(info: DropInfo) {
//        vm.draggingItem = nil
        vm.isDraggingInDelView = false
    }
    /// Object is dragged over the onDrop view.
    func dropEntered(info: DropInfo) {
        vm.isDraggingInDelView = true
    }
}

struct ImageMoveItem: Identifiable, Hashable{//, Transferable
    var id = UUID()
    var uiImage: UIImage
    var hasAdd:Bool = false
//    static var transferRepresentation: some TransferRepresentation {
//        DataRepresentation(exportedContentType: .png) { value in
//            return value.uiImage.pngData()!
//        }
//    }
}

//struct ImageMoveItem: Identifiable, Hashable, Transferable, Codable {
//    var id = UUID()
//    var uiImage: UIImage
//    var hasAdd: Bool = false
//    enum CodingKeys: String, CodingKey {
//        case id
//        case uiImageData
//        case hasAdd
//    }
//    init(uiImage: UIImage, hasAdd: Bool = false) {
//        self.uiImage = uiImage
//        self.hasAdd = hasAdd
//    }
//    init(from decoder: Decoder) throws {
//        let container = try decoder.container(keyedBy: CodingKeys.self)
//        id = try container.decode(UUID.self, forKey: .id)
//        hasAdd = try container.decode(Bool.self, forKey: .hasAdd)
//        
//        let uiImageData = try container.decode(Data.self, forKey: .uiImageData)
//        guard let image = UIImage(data: uiImageData) else {
//            throw DecodingError.dataCorruptedError(forKey: .uiImageData,
//                                                   in: container,
//                                                   debugDescription: "UIImage data is not valid")
//        }
//        uiImage = image
//    }
//    func encode(to encoder: Encoder) throws {
//        var container = encoder.container(keyedBy: CodingKeys.self)
//        try container.encode(id, forKey: .id)
//        try container.encode(hasAdd, forKey: .hasAdd)
//        
//        guard let uiImageData = uiImage.pngData() else {
//            throw EncodingError.invalidValue(uiImage,
//                                             EncodingError.Context(codingPath: [],
//                                                                   debugDescription: "UIImage could not be encoded"))
//        }
//        try container.encode(uiImageData, forKey: .uiImageData)
//    }
//    static var transferRepresentation: some TransferRepresentation {
//        CodableRepresentation(contentType: .item)
//    }
//}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容