개발/Swift

[Swift] Drag&Drop Interaction CustomValue 전달

덤벨로퍼 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)
}