이 글은 요청에 의해 작성되는 글입니다.

Summary

사이즈가 다른 Web Image를 가져와서 UITableView에 그릴때, UITableViewCell의 높이를 잡아야 하는 경우들이 있다.
일반적으로 텍스트들이나 가지고 있는 이미지를 사용하여 그려줄때는 미리 사이즈를 계산해서 작업을 하면되지만, Web Image는 각 이미지마다 크기나 네트워크 상황에 따라서 받아오는 속도가 다르기 때문에 미리 UITableViewCell의 높이를 고려해 줄 수 없다.
이런 경우에 사용하는 방법을 간단하게 알아보자
(웹이미지를 사용한 동적인 셀 그리지 예제이므로 이미지의 캐싱처리같은 고도화 관련 내용은 제외함)

0. 기본기

Self-Sizing Table View Cells

UITableViewAutomaticDimension, estimatedRowHeight
iOS 에서 이전에는 UITableView 자체에 rowHeight를 지정해주거나, UITableView delegate의 tableView:heightForRowAtIndexPath: 통해서 높이를 지정해 주었다.
iOS8 부터 UITableView에서 UITableViewCell의 높이를 동적으로 지정해줄 수 있는 Self-Sizing 방식이 추가되었다.
Self-Sizing 방식을 사용하려면 tableView의 rowHeight 의 속성을 UITableViewAutomaticDimension 로 설정하고, estimatedRowHeight 속성에도 값을 할당해 주어야 한다. (아래 코드를 참고하자)
tableView.estimatedRowHeight = 85.0
tableView.rowHeight = UITableViewAutomaticDimension

1. 프로젝트 생성 및 테이블뷰 추가

본격적으로 프로젝트를 생성하고 코드를 구현하면서 예제를 진행해보자.
우선 Swift 프로젝트 하나를 생성하고, storyboard 에 tableView 를 추가한 후 constraints 값을 지정하고 ViewController에 기본 tableView 설정을 해주도록 하자.

storyboard에 tableView 추가

그냥 TableView를 view 위에 얹어주면 된다.

TableView의  Constraints 설정

view를 모두 채우기 위해 margin을 모두 0으로주자.

TableView의 outlets을 ViewController에 연결

tableView를 선택하고 오른쪽버튼을 누른채로 ViewController로 끌어주면 Outlets 연결 화면이 뜬다.
여기서 dataSourcedelegate를 연결해주자

ViewController에 tableView 객체 추가

Xcode 우측위에 Assistant editor (oo 두개가 겹쳐진 버튼)을 눌러 ViewController를 열어주고 storyboar의 객체를 오른쪽버튼을 누른채로 끌어넣어주면 바로 코드에 추가할 수 있다.

TableView 사용을 위한 기본 코드 추가

storyboard 연결은 모두 끝이 낫으니 tableView를 사용할때 필요한 필수 코드들을 추가하고, 실행을 한번 해보자.
tableView를 사용하기위해 UITableViewDataSource protocol을 추가하고, 필수 함수인 numberOfRowsInSectioncellForRow를 추가해 주어야 한다.
(이 예제에서 UITableViewDelegateheightForRow는 사용하지 않는다. rowHeight를 자동으로 처리할 거니까)
아래 코드를 보자.
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - UITableViewDataSource
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
}
이렇게 코드를 작성하고 run을 해보면 아래처럼 tableView가 보여지게 된다.

tableView가 그려졌으니 50%는 끝이 났다! (사실 글을 쓰기위해 사진찍는 작업이 거의 다 끝났다!)

2. TableView self-sizing 적용, CustomCell class 생성, storyboard 연결

웹의 이미지를 가져와 보여줄 cell의 기본 틀을 잡아보자.
(이 예제에서는 label과 imageView를 cell에 얹어서 작업할 것이다.)
기본 UITableViewCell 위에 UILabel, UIImageView를 추가할 수도 있지만, 더 편하게 관리하고 작업하기 위해 CustomCell 을 구성해서 사용하자.

CustomCell Class 추가

필요한 내용으로만 직접 구성해서 사용할 CustomCell Class를 추가해보자. 
(우선은 Class만 생성하고 label, imageView는 storyboard에서 추가하자.)
class ViewController: ~~ {
...
}
// ViewController CustomCell class
class SelfSizingTableCell: UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
코드로 구현할 경우 tableView에 custom cell을 등록하는 register 함수를 사용해야 하는데, 이 예제에서는 storyboard에서 직접 설정을 할 것이기 때문에 cell register 코드는 추가하지 않아도 된다.

TableView Self-Sizing적용

사이즈를 알 수 없는 Web Image를 받아와서 그려줘야 하기 때문에 tableView에다 위에 기본기 부분에서 봤던 self-sizing 설정을 추가해주자
import UIKit
private let cellId = "SelfSizingCellId"
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 20
}
...

Storyboard 에서 Cell 구성 - 기본

storyboard에서 cell을 구성하기 위해 우선 TableView에 cell을 하나 추가해주자.
TableView아래에 cell을 끌어서 넣어주면 아래 화면과 같이 Prototype Cells 화면이 구성된다.


추가한 Cell을 선택 후 Identity inspector를 열어서 조금전 추가한 class를 연결해주자.
생성한 CustomCell class의 이름을 넣어주면 된다.


그리고 tableView에서 reusableCell 사용시 연결될 수 있게 cell의 Attributes inspector를 열어서 Identifier를 설정해주자.


(option) Cell 설정 마지막으로 그냥 작업할때 보기 편하게 하기위해 Size inspector 에서 rowHeight를 조금 조절해주자

이건 그냥 진짜 storyboard 그릴때 편하기 위해서 사용하는 용도이다.

Storyboard 에서 Cell 구성 - label/image 추가

이제 내용을 출력해줄 label과 image를 추가하고 코드로 연결해주자
label이 위에있고, imageView가 아래에 있는 형태로 구성할 예정이다.

label 하나를 cell의 contentView 아래에 추가해주고 constraints 값을 설정해주자. label의 constraints 값은 아래에 imageView를 붙일것이기 때문에 bottom 마진은 설정하지 않는다.


imageView도 label과 같은 방법으로 추가해주는데, 깔끔하게 작업하기 위해 대충 원하는 크기로 이미지뷰 크기를 조절해두고 constraints의 모든 마진을 0으로 주면 top은 label의 bottom과 연결되고 나머지는 cell에 맞춰 마진이 설정된다.

이렇게 imageView의 크기를 조절해서 자리를 잡아주자


그리고 constraints 에서 마진 설정을 해주면!
Xcode는 똑똑하기 때문에 알아서 원하는대로 아래처럼 마진을 잡아준다.

label


imageView


결과는 이렇게 된다.

Cell 구성요소 코드에 연결

이제 다시 Assistant editor를 열어서 한쪽은 storyboard 한쪽은 코드 구현부를 열어주자.
그리고 방금전 추가한 label과 imageView를 SelfSizingTableCell class 에 tableView 객체를 생성해준것처럼 생성해주자.
편의상 label은 titleLabel로, imageViewwebImage로 네이밍하도록 하자.
연결이 되면 코드상에 아래처럼 구성이 된다.
class SelfSizingTableCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var webImage: UIImageView!
...

Cell 사용부 수정 (cellForRow)

이제 생성한 SelfSizingTableCell을 사용할 수 있도록 tableView에서 cell을 가져오는 부분을 수정해주자.
reusableCell 로 구성하였기 때문에 기존에 cell 사용과 달리 dequeueReusableCell을 사용하여 가져오도록 수정해주고 cell의 class를 명시해주면 된다.
...
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: SelfSizingTableCell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! SelfSizingTableCell
cell.titleLabel.text = "\(indexPath.row)"
return cell
}
...

3. Web image를 받아와 Self-Sizing cell에 그려주기

저장된 이미지를 사용하는 경우는 그냥 위의 예제까지만 하고 cell에 image를 설정해주면 끝이난다.
하지만 이 예제는 웹 이미지를 다운받고 셀의 높이를 변경해주는게 궁극적인 목표이기 때문에 우선 web image를 받아오는 예제를 살펴보자.

(UIImageView에서는 기본적으로 웹이미지를 받아와서 image를 설정해주는 함수가 존재하지 않는다.
alamofire나 sdwebimage 같은 유명한 open source를 사용하면 쉽게 우리가 만드려는 기능을 사용할 수 있지만, 어떤식으로 처리되는지 이해하기 쉽도록 open source를 사용하지 않고 만들어보자.)

UIImageView web image를 위한 extension 추가

UIImageView에서는 기본적으로 웹이미지를 받아와서 image를 설정해주는 함수가 존재하지 않는다.
alamofiresdwebimage 같은 유명한 open source를 사용하면 쉽게 지금 만들려는 기능을 구현 수 있지만, 어떤식으로 처리되는지 기본기를 익혀볼 겸 open source를 사용하지 않고 image data를 받아와서 처리하도록 만들어보자. (간단한 진행을 위해 캐싱은 구현하지 않음)
그리고, image를 web에서 받아오기까지 시간이 걸릴 수 있기 때문에 보통 indicator를 보여주거나 placeholder 이미지를 넣어두고 다운로드가 완료되면 받아온 image 로 교체하여 사용한다. 이 예제에서는 placeholder를 이용하도록 하겠다.

위에 구현중이던 ViewController에 아래와 같이 extension 기능을 구현해주자.
...
extension UIImageView {
public func imageFromURL(urlString: String, placeholder: UIImage?, completion: @escaping () -> ()) {
if self.image == nil {
self.image = placeholder
}
URLSession.shared.dataTask(with: NSURL(string: urlString)! as URL, completionHandler: { (data, response, error) -> Void in
if error != nil {
print(error)
return
}
DispatchQueue.main.async(execute: { () -> Void in
let image = UIImage(data: data!)
self.image = image
self.setNeedsLayout()
completion()
})
}).resume()
}
}
이 코드는 UIImageView에 extension을 사용하여 기능을 확장해준 것이다.
코드를 보면 web에 있는 image를 받아오기 전까지 image가 nil 값인 경우 지정해준 placeholder 이미지로 imageView를 채워준 후
URLSession을 사용해서 image url에 있는 data를 받아와서 imageView의 image를 변경해주고, 후행 클로저를 사용해 completion을 호출해 주도록 되어 있다.
completion을 사용하는 이유는 tableView에서 이미지 변경 후 셀을 갱신해줄 수 있도록 하려고 해두려고 한 것이다.
후행 클로저라는 용어가 생소할 수도 있는데, 그냥 동작이 완료된 후 처리할 묶음을간단히 말하면 호출한 곳에서 처리가 완료된 후에 작업을 할 수 있도록 통로를 만들어 둔다고 생각하면 된다.

이미지 중복 갱신 방지

image를 처음 받아와 그려줄때 갱신이 안되는 이슈가 발생할 수 있기 때문에 1회에 한해서 reloadRows 라는 함수를 사용할 예정인데, cell이 그려질때마다 이 함수가 호출되면 부하가 심해지고 스크롤이 버벅이는 현상이 생길수가 있다.
이것을 막기위해 갱신을 했는지 상태값을 저장해두고 사용하도록 SelfSizingTableCell 코드에 finishReload 라는 Bool 타입의 변수를 하나 추가해주자. 
이 값은 초기값으로 false값을 가지고, 그리기가 완료 true 값으로 변경해 불필요한 갱신을 막는 용도로 사용할 것이다.
class SelfSizingTableCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var webImage: UIImageView!
var finishReload: Bool = false
...
}

cell에 web image 세팅하기

이제 마지막 단계이다. 
위에서 extension으로 구현해둔 imageFromURL함수를 이용해 cell에 webImage를 web에서 받아와서 세팅해보자.
...
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.webImage.tag = indexPath.row
let urlStr: String = "https://placehold.it/200x200&text=SampleImg\(indexPath.row)"
let placeholder: UIImage? = UIImage.init(named: "placeholder.png")
cell.webImage.imageFromURL(urlString: urlStr, placeholder: placeholder) {
if cell.finishReload == false {
cell.finishReload = true
tableView.beginUpdates()
tableView.reloadRows(at: [IndexPath.init(row: cell.webImage.tag, section: 0)], with: UITableViewRowAnimation.automatic)
tableView.endUpdates()
}
}
return cell
}
...
이 코드를 보면 cellForRow~ 함수 내부에서 cell의 webImage를 세팅할때 imageFromURL 함수를 호출해서 이미지를 받아온다.
이미지를 받아온 후 finishReload 값을 비교해서 cell 갱신이 완료되지 않았으면 reloadRows 함수를 이용해 cell을 한번 갱신해주고, 다시 호출되지 않도록 finishReload 값을 true로 설정해 준다.

reloadRowstableView 전체를 갱신하는 reloadData와 달리 선택한 row들만 갱신해주는 역할을 한다.

저장되어있는 이미지나 캐싱된 이미지를 사용할 경우 reloadRows 동작은 필요하진 않다. 언제 받아올지 모르는 웹 이미지를 받아온 후 알아서 갱신을 해주면 좋겠지만, 이미 tableView를 그려주는 life cycle이 끝났기 때문에 화면을 가만히 놔둔 상태에서 이미지는 바뀌어도 크기가 바뀌지 않는 경우들이 많다.
finishReloadreloadRows는 이런경우 바로 갱신을 해주기 위해 사용하는 트릭으로 생각하면 좋을것 같다. View life cycle을 
(urlStr 값은 파라미터로 문자를 넣어주면 이미지로 만들어주는 url 주소이다. placeholder.png 파일은 없으면 그냥 nil값을 사용해도 무관하다.)

4. 실행/확인

이제 모든 구현이 완료되었고, 구동을 해보자!


처음엔 placeholder 이미지가 -> 그리고는 다운로드된 이미지가 -> 그리고 셀을 다시 그려 높이를 맞춰준다.

Fin.

간단하게 autolayout으로 요소들을 구성하고, 웹 이미지를 받아와서 동적으로 받아온 이미지 크기로 셀 높이를 맞춰서 그려주는 예제를 해보았다.
별거 아닌거 같지만 까다로운 요소들이 많이 숨어있는 작업이다. 
또한, 같은 기능을 만드는 방법에 정석이 따로 있는것은 아니다. 여기서는 트릭을 이용해서 처리를 했지만 다른 더 좋은 방법들이 존재할 수 있다.
하지만 뭐니뭐니해도 가장 좋은건 이미 잘 알려진 open source를 사용하면 아주 간단하고 안정성이 보장되도록 만들수가 있다.
따라하기를 기반으로 대충 이런식으로 처리가 되는구나 정도를 생각하면서 개념을 익힌 다음에는 직접 고도화시켜서 구현을 해서 open source를 만들거나 open source를 사용하자!!


반응형