포스트

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
    // ...
}

그러니 리뷰 상세 페이지에서 할 일은 두 가지였다.

  1. 사진을 눌렀을 때 그 사진이 몇 번째인지 알아내기
  2. 리뷰의 사진 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"
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.