ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Swift] Drag&Drop Interaction CustomValue 전달
    개발/Swift 2022. 5. 2. 15:22

    요구사항

    • 뷰는 UIViewController 와 UITableView가있다.
    • UITableView에 있는 아이콘을 드래그하여 UIview에 드랍해야한다.
    • 드래그 → 드랍 과정에서 데이터가 오가야한다. (Model)
    • UIView 에 드랍이 되면 UITableView에 있는 아이콘을 드랍하는게 아니라 다른 View를 그려야한다.

     

    일반적인 Drag & Drop 은 UIImage를 전달한다.

    하지만 요구사항에 충족하려면 UIImage를 전달해서는 안된다. 어떤 데이터를 받고 그 데이터를 기반으로 새로운 View를 그려야한다.

    그러려면 전달하려는 데이터의 클래스(타입)이 ``NSItemProviderReading , NSItemProviderWriting 을 상속하면 된다. 그렇지 않으면 dropSession에서 해당 클래스를 Load 하지 못한다.

    CustomModule 이라는 타입을 만들었다.

    내부 변수로는 CustomModuleType라는 enum 타입을 두었다.

    enum 타입을 변수로 두려면

    해당 enum 타입도 Codable 을 상속 해야한다.

    String 같은 기본타입은 그냥 넣어주면 된다.

    final class CustomModule : NSObject , NSItemProviderWriting , Codable,NSItemProviderReading {
        
        let type : CustomModuleType
        
        init(type:CustomModuleType) {
            self.type = type
        }
    
        static var writableTypeIdentifiersForItemProvider: [String] {
            return [String(kUTTypeData)]
        }
        
        func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
            let progress = Progress(totalUnitCount: 100)
                do {
                    let data = try JSONEncoder().encode(self)
                    progress.completedUnitCount = 100
                    completionHandler(data, nil)
                } catch {
                    completionHandler(nil, error)
                }
                return progress
        }
        static var readableTypeIdentifiersForItemProvider: [String] {
               return [String(kUTTypeData)]
           }
           
        static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> CustomModule {
           do {
               let subject = try JSONDecoder().decode(CustomModule.self, from: data)
               return subject
           } catch {
               fatalError()
           }
        }
        
        
    }
    
    enum CustomModuleType : Codable {
        
        case Button
        case Switch
        
        enum ErrorType: Error {
                case encoding
                case decoding
            }
        init(from decoder: Decoder) throws {
            let value = try decoder.singleValueContainer()
            let decodedValue = try value.decode(String.self)
            switch decodedValue {
            case "button":
                self = .Button
            case "switch":
                self = .Switch
            
            default:
                throw ErrorType.decoding
            }
        }
        
        
        func encode(to encoder: Encoder) throws {
            var container = try encoder.singleValueContainer()
            switch self {
            case .Button:
                try container.encode("button")
            case .Switch:
                try container.encode("switch")
            }
        }
    }
    

    NSItemProviderWriting ,NSItemProviderReading 을 상속하면 저 위의 함수들을 구현해줘야하는데

    내부의 내용은 이해하지못한다. 단지 read / write 를 하는데 필요한 encoding / decoding 작업을 해주는거로 판단된다. 커스텀 클래스를 사용하려면 위 코드를 그대로 사용해도 된다.

    우선 드래그가 시작될 TableView를 담은 ViewController 에서

    UITableViewDragDelegate,UITableViewDropDelegate 를 상속받아 줘야 Drag / Drop 이벤트가 가능해진다.

    테이블 뷰에서는 해당 moduleList를 보고 cell들을 그렸다

    let modulelist = [CustomModule(type: .Button) , CustomModule(type: .Switch)]
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            modulelist.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "CustomModuleListTableViewCell", for: indexPath) as? CustomModuleListTableViewCell else{ fatalError("cell error")}
            let image = getImage(type: modulelist[indexPath.row].type)
            cell.moduleImageView.image = image
            return cell
        }
        
        private func getImage(type:CustomModuleType) -> UIImage?{
           switch type {
           case .Button:
               return UIImage(named: "lineBtn")
           case .Switch:
               return UIImage(named: "lineSwitch")
           default:
               return UIImage()
           }
       }
    

    Drag & Drop으로 데이터를 전달할건데

    CustomModule(type: .Button) 라는 객체를 전달 해줄것이다.

    func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
            print("itemsForBeginning")
            let module = modulelist[indexPath.row]
            let provider = NSItemProvider(object: module)
            return [UIDragItem(itemProvider: provider)]
        }
    

    전달할 객체를 NSItemProvider 에 담아

    UIDragItem에 넣어 리턴해주었다.

    이제 드래그 이벤트가 실행되었다.

    여기서부터는 별도의 작업을 해주지않아도 테이블뷰에서 시작된 DragItem을

    드랍할 위치에있는 UIViewController에서 Drop을 받을수있다.

    드랍을 받은 UIViewController에서도(앞으로 DropViewController라 칭한다)

    DropDelegate를 상속받아주었다.

    만약 DropViewController 위치에 드래그 할 아이템이 오면 해당 함수가 한번 실행된다.

    func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
            
            return session.canLoadObjects(ofClass: CustomModule.self)
        }
    

    CustomModule 이라는 타입을 Load할수 있는지 체크한다 true인경우 다음 로직이 실행되고

    아니면 이후 로직이 모두 cancel 된다. 즉 true가 나와야 Drop이 가능하다.

    True라면 다음 로직이 실행된다. 아래 함수는 DragItem의 위치가 변경될때마다 호출된다.

    func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
            
            if customModuleListVC.moduleListTableView.hasActiveDrag {
                return UIDropProposal(operation: .copy)
            }else{
                return UIDropProposal(operation: .move)
    
            }
    
        }
    

    session 을 제공해주는데 session에서는 위치값등 여러 정보들을 제공해준다.

    상황에따라 다른 처리를 한다면 session 을사용하면 좋다.

    나의 경우는 TableView에서 온 DragItem이라면 복사

    해당 UIViewController 에서 온 DragItem이면 이동을 하기위해 처리했다.

    이제 손을 떄면 DragItem이 드랍된다.

    이시점에 해당 함수가 호출된다.

    func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
    	session.loadObjects(ofClass: CustomModule.self) {[weak self] items in
                guard let self = self ,
                      let customModule = items.first as? CustomModule else { return }
                let location = session.location(in: self.view) 
    	}
    }
    
    

    역시 session 을 제공해준다.

    session.loadObjects(ofClass: CustomModule.self) { }
    

    위 함수를 사용하여 전달받을 CustomModule 객체를 받아올수있다.

    location 정보역시 받아왔다.

    이제 해당 정보에 따라 원하는 View를 그려주면 된다.

    func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
            session.loadObjects(ofClass: CustomModule.self) {[weak self] items in
                guard let self = self ,
                      let customModule = items.first as? CustomModule else { return }
                let location = session.location(in: self.view)
                print("type \\(customModule.type) location \\(location)")
                
                switch customModule.type {
                case .Button:
                    let container = addContainer()
                    
                    container.snp.makeConstraints { maker in
                        maker.width.height.equalTo(128)
                        maker.center.equalTo(location)
                    }
                    self.addChildVC(CustomButtonModuleViewController(), container: container)
    
                case .Switch:
                    let container = addContainer()
                    container.snp.makeConstraints { maker in
                        maker.width.equalTo(128)
                        maker.height.equalTo(80)
                        maker.center.equalTo(location)
                    }
                    self.addChildVC(CustomSwitchModuleViewController(), container: container)
                    return
                }
            }
    }
    
    func addContainer() -> UIView {
        let container = UIView()
        container.backgroundColor = .white
        self.view.addSubview(container)
        return container
    }
    
    func addChildVC(_ childVC : UIViewController,container:UIView){
        addChild(childVC)
        childVC.view.frame = container.bounds
        container.addSubview(childVC.view)
        childVC.willMove(toParent: self)
        childVC.didMove(toParent: self)
    }
    

    댓글

Designed by Tistory.