ios/SwiftUI

#4 SwiftUI의 Coordinator

ally10 2020. 8. 25. 15:24

이번에는 SwiftUI의 ConnectorCoordinator에 대한 개념에 대해 작성해보고자 한다.

 

이전 포스팅에서는 @Binding property로 SwiftUI에서 세팅된 변수를 UIKit에서 읽어들일 수 있었는데, 이번엔 이와 비슷하게 SwiftUI-UIKit간의 브릿지 역할을 하는Coordinator에 대해서 알아보자.

 

@Binding property의 경우 SwiftUI에서 뭐 뷰가 나타났을때 세팅을 해준다던가, 버튼을 클릭했을때 토글을 해준다던가..의 형태로 @State를 설정해주면, UIKit코드에서 이 값을 read-only로 읽어들일 수 있었다. read-only라는점이 중요하다. 바인딩 프로퍼티로는 반대입장, 즉 UIKit의 변수를 SwiftUI로 전달할 수 없다는 것이다. 이 반대의 개념은 UIKit와 SwiftUI를 섞어쓰다보면 필수적이다. 간단하게는.. 테이블뷰를 UIKit으로 생성했다면, 테이블뷰의 칼럼 값을 SwiftUI로 전달해서 띄워야 할 필요가 있을수도 있다.

 

이를 위해서 존재하는게 바로Coordinator다.

간단하게 요약하자면, @Binding property는 SwiftUI -> UIKit 으로의 변수 전달이고, Coordinator의 경우 UIKit -> SwiftUI로의 데이터 전달이라고 생각하면 쉽다.Coordinator라고 해서 새로운 개념 같지만, 사실상 "delegate"의 역할을 한다고 봐도 무방하다. 테이블뷰로 예를 들었는데, 테이블뷰의 TableViewDelegate같은 것들이 Coordinator의 역할을 한다. 

 

TableView로 계속해서 설명했으니 TableView예시로 보자.

 

struct TableView: UIViewRepresentable {
    @Binding var presented: Bool
    
    func makeUIView(context: Context) -> UITableView {
        let view = UITableView()
        
        return view
    }
    
    func updateUIView(_ uiView: UITableView, context: Context) {
        if self.presented {
            
        }
    }
}

 

일단 이전 포스팅에서 설명한것처럼 UIKit의 TableView를 UIRepresentable로 감싼 struct를 생성해준다. SwiftUI의 presented라는 바인딩 프로퍼티를 읽어들여서 상태에 따라서 칼럼값을 업데이트 하고자 한다.

 

struct ContentView: View {
    @State var presented: Bool = false
    
    var body: some View {
        VStack {
            TableView(presented: $presented)
            .onAppear(perform: {
                self.presented = true
            })
        }
    }
}

 

이거는 SwiftUI의 컨텐츠뷰~ 전 포스팅과 비슷하게 Table뷰를 스택으로 감쌌다. 여기서 눈에 띄는게 onAppear이라는 함수인데, SwiftUI View의 라이프 사이클 상태에 따라서 호출되는 함수다. 주의할것은 UIView의 라이프 사이클이 아니라는것! 아마..비슷하긴 할테지만... 무튼, onAppear가 호출 될 때 컨텐츠뷰의 presented라는 프로퍼티를 true로 세팅해준다. 즉, SwiftUI의 컨텐츠뷰가 나타날 때 presetned라는 프로퍼티가 true로 트리거되고, 이 프로퍼티가 UIView가 읽어들일 수 있다. 

 

이 컨텐츠뷰가 나타났을 때, presented 가 true로 세팅되고, 이 변수를 읽어들여 UITableView의 칼럼값을 업데이트 하고자 한다.

 

일단 바인딩 프로퍼티에 의해 트리거되는 presented 변수는 바인딩 프로퍼티로 SwiftUI -> UIKit으로 전달이 될 테고, 이는 updateUIview에서 전달받을 수 있을 것이다. 즉, 아래 updateUI의 presented 가 true가 되었을 때 칼럼을 채워주는 함수를 작성해야한다. UITableView를 다뤄본 사람들은 알겠지만, 칼럼값을 채우기 위해서는 아래의 TableViewDelegate 프로토콜을 구현해주어야 한다.

 

이를 위해서 사용하는것이 바로 Coordinator다!

 

아래와 같이 테이블뷰 스트럭쳐 내부에 테이블뷰 delegate를 adopt한 coordinator 클래스를 만들어준다.

 

struct TableView: UIViewRepresentable {
    @Binding var presented: Bool
    
    func makeUIView(context: Context) -> UITableView {
        let view = UITableView()

        return view
    }
    
    func updateUIView(_ uiView: UITableView, context: Context) {
        if self.presented {
            uiView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
            uiView.delegate = context.coordinator
            uiView.dataSource = context.coordinator
        }
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
    
    class Coordinator: NSObject, UITableViewDelegate, UITableViewDataSource {
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
            
            cell.textLabel?.text = _contents[indexPath.row]
            
            return cell
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return _contents.count
        }
        
        var _contents = [
            "Apple",
            "Banana",
            "Melon"
        ]
    }
}

 

폼은 대충 위와 같다. 테이블 뷰 안에 UITableViewDelegate, UITableViewDataSource를 어답트한 코디네이터 클래스를 만들면 struct 

tableView가 프로토콜을 만족하지 않는다는 에러가 뜨는데, 위 코드에서처럼 makeCoordinator함수를 추가해주면 된다.

차근차근 설명해보자면 makeCoordinator는 말그대로 Cooridnator를 만드는 함수고, 코디네이터 클래스는 UIView -> SwiftUI로의 브릿지 역할을 하는 delegate라고 보면 된다. 

 

일단 binding property에 따라 업데이트하는 updateUIView함수를 보면,

    func updateUIView(_ uiView: UITableView, context: Context) {
        if self.presented {
            uiView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
            uiView.delegate = context.coordinator
            uiView.dataSource = context.coordinator
        }
    }

 

presented 변수가 True로 토글되었을 때, TableView resigter및 delegate, datasource를 지정해준다. coordinator는 context에 의해 makeCoordinator() 함수를 통해 생성된다.

 

    class Coordinator: NSObject, UITableViewDelegate, UITableViewDataSource {
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
            
            cell.textLabel?.text = _contents[indexPath.row]
            
            return cell
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return _contents.count
        }
        
        var _contents = [
            "Apple",
            "Banana",
            "Melon"
        ]
    }

코디네이터는 위와 같이 정의한다. 칼럼값을 내부 변수로 3가지로 정의해두고, 기존 tableView delegate, datasource와 같이 함수를 작성해주면 된다.

 

 

 

짠~ 위와 같이 테이블뷰가 정상적으로 보인다!

 

이 포스팅을 요약해보자면 아래와 같다.

일단, UITableView를 UIRepresentable로 감싸고 UITableView의 delegate, datasource를 Coordinator로 생성해준다. 그리고 그에 따른 메소드는 모두 이 코디네이터 안에 정의해준다. 그리고 SwiftUI의 뷰가 나타날 때 SwiftUI 콘텐츠뷰에 선언된 presented라는 변수를 true로 세팅하고, UIView에서는 이 변수의 변화를 감지하여 true로 세팅될 때, 즉 View가 나타날 때 테이블뷰는 라이프사이클에 따라 Coordinator에 정의된 메소드대로 동작한다.

 

 

사실 쓰다보니 굳이 이렇게 해야되나? 그냥 UIView쓰면 안됨? 싶겠지만... SwiftUI를 꼭!!!!! 써야하고, 굳~~~이 UIKit을 사용하고 싶을 때 이렇게 사용하면 된다. 사실 swiftUI가 나온지도 얼마 안됐고, 보편화도 안됐기 때문에 뭔가 아직 번거로운 느낌이나 굳이 스러운 느낌은 있다..ㅋㅋㅋ

 

다음번엔 UIHostingController에 대해서 포스팅하고자 한다! UIHostingController는 UIRepresentable의 정 반대의 개념이라고 생각하면 된다. UIRepresentable은 UIKit을 SwiftUI에 포팅하는 개념이고, UIHostingController는 SwiftUI의 View를 UIKit 으로 포팅하는 개념이다!