프로토콜 지향 프로그래밍을 이용한 프로젝트를 진행해보자 (1)

프로토콜 지향 프로그래밍에 관해 공부를 하다가 우연히 해당 발표를 찾아서 여기에 나와있는 프로젝트를 진행해 보았습니다.

 https://www.youtube.com/watch?v=9gkzHUsQiUc&t=1138s

저희는 이제 사진첩 앱을 개발하려고 합니다. 사진첩은 테이블 뷰 모드와 콜렉션 뷰 모드를 지원하고, 각 셀을 클릭 하면 이미지를 크게 볼 수 있는 화면으로 이동합니다.

테이블뷰, 콜렉션뷰, 상세화면

딱 보기에도 중복되 보이는 기능이 여럿 보입니다. 이미지와 설명, 테이블뷰와 콜렉션뷰는 같은 이미지를 저장하고 있는 배열을 공유하고, 셀을 클릭하였을때 동일한 페이지로 이동하는 기능이 중복되어 있습니다. 

일단 UI부터 구성을 해보겠습니다. 테이블뷰와 콜렉션뷰는 이미지와 이미지 설명을 각각 가지고 있습니다. 이를 프로토콜로 선언해 중복되는 코드 없이 구현을 해보도록 하겠습니다.

protocol MediaContainer {
    var content: Content? { get set }
    var media: UIImageView! { get }
    var note: UILabel! { get set }
    
    func contentUpdate()
}

저의 경우에는 스토리 보드로 UI를 만들어서 강제 언래핑을 했는데, 코드로 UI를 만들었을경우 강제 언래핑을 안하는것이 좋을것 같네요.

해당 셀에 content값이 들어오면 media와 note를 업데이트 해주도록 extension으로 기초 구현을 해주겠습니다.

extension MediaContainer {
    func contentUpdate() {
        if let content = content {
            media.load(url: content.url)
            note.text = content.description
        }
    }
}

이제 테이블/콜렉션 뷰 셀을 만들때 해당 프로토콜을 채택 해주면 됩니다.

class ContentTableViewCell: UITableViewCell ,MediaContainer {
    
    static let identifier = "ContentTableViewCell"
    
    @IBOutlet weak var media: UIImageView!
    
    @IBOutlet weak var note: UILabel!
    
    
    
    var content: Content? {
        didSet {
            contentUpdate()
        }
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }
}
class ContentCollectionViewCell: UICollectionViewCell ,MediaContainer{
    
    static let identifier = "ContentCollectionViewCell"
    
    @IBOutlet weak var media: UIImageView!
    
    @IBOutlet weak var note: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }
    
    var content: Content? {
        didSet {
            contentUpdate()
        }
    }
}

contentUpdate()를 프로토콜에서 extension으로 이미 구현을 하고 있어서 같은 코드를 2개를 구현할 필요가 없어졌습니다. 프로토콜을 채택하고, 사용하면 됩니다.

 

이번에는 테이블/ 콜렉션 뷰가 같은 이미지 리스트를 공유하니 이미지 리스트를 가지고 있는 객체를 하나 만들겠습니다.

class TimelineContentObject {
    static let shared = TimelineContentObject()
    var contents: [Content] = [Content]()
}

해당 객체의 contents를 사용하려면 우리는 보통

TimelineContentObject.shared.contents

이런식으로 싱글톤 객체에 접근에 사용을 합니다. 하지만 각 뷰컨트롤러에서 이런 코드를 중복구현을 하는게 싫다! 이럴때 역시 프로토콜을 사용해 코드의 중복 사용을 없앨수 있습니다.

protocol ContainContents {
    var contents: [Content] { get }
}

extension ContainContents {
    var contents: [Content] {
        return TimelineContentObject.shared.contents
    }
}

이제 이미지 배열에 접근하여 사용하려고 하면 ContainContents를 채택하기만 하면 싱글톤 객체의 contents를 사용 할 수 있습니다. 

class GalleryTableViewController: UITableViewController, ContainContents
class GalleryCollectionViewController: UICollectionViewController, ContainContents

테이블/ 콜렉션 뷰는 각 셀을 클릭하면 해당 사진의 이미지와 설명을 좀 더 크게 보여주는 상세 페이지로 이동합니다. 이 기능역시 프로토콜로 구현을 해서 채택을 해서 중복적인 코드 없이 기능을 사용 할 수 있습니다. 

protocol CanShowDetailView{
    func showDetailView(withContent content: Content)
    var navigationController: UINavigationController? { get }
}

extension CanShowDetailView where Self:UIViewController {
    func showDetailView(withContent content: Content) {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        guard let newVC = storyboard.instantiateViewController(identifier: "DetailView") as? DetailViewController else { return }
        newVC.content = content
        self.navigationController?.pushViewController(newVC, animated: true)
    }
}

이제 해당 프로토콜을 채택하고 셀이 선택되었을때 showDetailView를 호출하기만 하면 됩니다. 

이렇게 테이블/콜렉션 뷰 모드가 지원되는 갤러리 앱이 완성되었습니다!