Final (23)
프로젝트 막바지에는 기능 구현에 쫓겨 블로그 정리를 제대로 하지 못했다. 그래서 이 글은 당시 GitHub 커밋 내역과, 프로젝트 기간에 직접 적어 둔 업무 내역 목록을 AI(Claude Code)에게 보여주고 그걸 바탕으로 내용을 다시 정리했다. 코드는 실제 커밋에 있던 것을 가져왔다.
리뷰 상세 페이지의 사진을 누르면 전체 화면으로 크게 볼 수 있게 했다. 진문님이 추천 상세페이지에서 만든 전체 화면 VC 가 이미 있어서, 그걸 재활용했다.
리뷰 이미지 확대
진문님의 FullscreenPageViewController 는 이미지 URL 배열과 시작 위치(currentIndex)를 받아서, 좌우로 넘기며 볼 수 있는 전체 화면을 띄운다. UIPageViewController(책장 넘기듯 화면을 한 장씩 넘기는 VC) 를 상속해서 만든 것이다.
1
2
3
4
5
6
class FullscreenPageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
var imageURLs: [URL] = []
var currentIndex: Int = 0
// ...
}
그러니 리뷰 상세 페이지에서 할 일은 두 가지였다.
- 사진을 눌렀을 때 그 사진이 몇 번째인지 알아내기
- 리뷰의 사진 URL 배열과 그 위치를 넘겨서 띄우기
문제는 1번이었다. 사진을 누르면 tap gesture 의 sender.view 로 눌린 UIImageView 는 알 수 있는데, 그 imageView 에 어떤 URL 의 사진이 들어 있는지는 알 방법이 없었다.
Associated Object란?
Swift 의 extension 은 메서드나 계산 property 는 추가할 수 있지만, 값을 저장하는 property(stored property)는 추가할 수 없다. 그래서 UIImageView 에 imageURL 이라는 칸을 그냥 만들 수가 없다.
Associated Object 는 Objective-C runtime 이 제공하는 기능으로, 이미 있는 객체에 값을 “꼬리표처럼” 붙여 둘 수 있게 해 준다. 택배 상자에 송장을 붙이는 것과 비슷하다. 상자(UIImageView) 자체를 바꾸지 않고, 바깥에 key 로 구분되는 값을 붙여 두었다가 나중에 같은 key 로 다시 읽는다.
이건 GPT 의 도움을 받아서 만들었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import UIKit
import Kingfisher
extension UIImageView {
private struct AssociatedKeys {
static var urlKey = "urlKey"
}
var imageURL: URL? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.urlKey) as? URL
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
imageURL 은 계산 property 라서 extension 에 넣을 수 있다. 값을 넣으면 objc_setAssociatedObject 로 imageView 에 붙이고, 읽으면 objc_getAssociatedObject 로 같은 key 의 값을 꺼낸다. key 로는 urlKey 변수의 메모리 주소(&)를 쓴다.
사진에 URL 붙이고 tap 연결하기
리뷰 사진은 1장일 때와 여러 장일 때 그리는 코드가 나뉘어 있어서, 두 곳 모두 같은 처리를 넣었다. 사진 1장일 때는 이렇다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
} else if imageURLs.count == 1, let imageURLString = imageURLs.first, let imageURL = URL(string: imageURLString) {
// ... 생략
imageView.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(imageTapped(_:)))
imageView.addGestureRecognizer(tap)
imageView.kf.setImage(with: imageURL) { [weak self] result in
switch result {
case .success(let value):
self?.imageView.imageURL = value.source.url
case .failure:
self?.imageView.imageURL = nil
}
}
}
UIImageView 는 기본적으로 터치를 받지 않아서 isUserInteractionEnabled 를 켜야 tap gesture 가 동작한다. Kingfisher 로 사진을 받는 데 성공하면, 그 사진의 URL 을 방금 만든 imageURL 에 붙여 둔다.
여러 장일 때는 반복문 안에서 imageView 마다 똑같이 했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for imageURLString in imageURLs {
guard let imageURL = URL(string: imageURLString) else { continue }
let tap = UITapGestureRecognizer(target: self, action: #selector(imageTapped(_:)))
// ... imageView 생성 생략
imageView.isUserInteractionEnabled = true
imageView.kf.setImage(with: imageURL) { result in
switch result {
case .success(let value):
imageView.imageURL = value.source.url
case .failure:
imageView.imageURL = nil
}
}
imageView.addGestureRecognizer(tap)
// ...
}
gesture recognizer 는 view 하나에만 붙을 수 있어서, imageView 마다 새로 만들었다.
눌린 사진 위치 찾아서 띄우기
눌린 imageView 에서 imageURL 을 꺼내고, 리뷰의 전체 사진 URL 배열에서 몇 번째인지 찾는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@objc func imageTapped(_ sender: UITapGestureRecognizer) {
guard let tappedImageView = sender.view as? UIImageView,
let tappedImageURL = tappedImageView.imageURL,
let reviewImages = reviewImages else { return }
let imageURLs = reviewImages.compactMap { URL(string: $0) }
if let currentIndex = imageURLs.firstIndex(of: tappedImageURL) {
let fullscreenPageVC = FullscreenPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
fullscreenPageVC.modalPresentationStyle = .fullScreen
fullscreenPageVC.imageURLs = imageURLs
fullscreenPageVC.currentIndex = currentIndex
self.present(fullscreenPageVC, animated: true, completion: nil)
}
}
transitionStyle: .scroll 로 좌우로 밀어서 넘기게 하고, .fullScreen 으로 띄운다. 2번째 사진을 눌렀으면 전체 화면도 2번째 사진부터 시작하고, 거기서 좌우로 다른 사진을 볼 수 있다.
같이 고친 것
리뷰 작성 화면의 CustomTextView 에 자동 수정과 맞춤법 검사를 끄고, 키보드를 밝은 색으로 고정했다. 글자색도 ThemeColor.mainBlack 으로 맞췄다.
1
2
3
4
textColor = ThemeColor.mainBlack
autocorrectionType = .no
spellCheckingType = .no
keyboardAppearance = .light
지난 글에서 ChatManager 에 문자열로 쓰던 "thread", "senderId" 도 Constants 로 옮겼다.
1
2
let db_thread = "thread"
let db_senderId = "senderId"