본문 바로가기
개발 이야기

[iOS 앱 개발] ImageView에 터치 기능 추가(@IBOutlet Collection)

by Jimmy_iOS 2023. 7. 23.

이미지를 버튼으로 만드는 방법

UI를 만들다 보면 버튼보다는 이미지를 넣어서 버튼처럼 동작하게 만들어야 할 때가 있다.

UIImageView는 기본적으로 터치를 인식할 수 없다. 

@IBOutlet weak var imageView: UIImageView!

override func viewDidLoad() {
    super.viewDidLoad()
    let tapGesture = UITapGestureRecognizer(
        target: self,
        action: #selector(imageTap(_:))
    )

    imageView.addGestureRecognizer(tapGesture)
    imageView.isUserInteractionEnabled = true
}

@objc func imageTap(_ sender: UITapGestureRecognizer) {
    print("tapped")
}

하지만 addGestureRecognizer()를 사용해서 터치를 인식하게 할 수 있는데

let tapGesture = UITapGestureRecognizer(
    target: self,
    action: #selector(imageTap(_:))
)

UITapGetureRecognizer(target:action:) 객체를 만들고 제스처를 인식했을 때 실행할 메서드도 추가해줍니다.

여러개의 이미지에 추가해줄 때 문제점

위 UI처럼 이미지가 많으면 StoryBoard에서 IBOutlet으로 일일히 연결해주게 되면 ViewController가 지저분해지게 된다.

@IBOutlet weak var imageView1: UIImageView!
@IBOutlet weak var imageView2: UIImageView!
@IBOutlet weak var imageView3: UIImageView!
@IBOutlet weak var imageView4: UIImageView!
@IBOutlet weak var imageView5: UIImageView!
.
.
.

이러한 문제점을 개선하기 위해 IBOutlet Collection을 사용하면 단 한 줄로 UI객체를 관리할 수 있다.

@IBOutlet var imageView: [UIImageView]!

하지만 여기서 또 문제점이 발생한다.

아래 코드를 실행하면 이미지를 탭했을 때 콘솔창에 “tapped”가 표시되지 않는다…

@IBOutlet var imageView: [UIImageView]!

override func viewDidLoad() {
    super.viewDidLoad()

    let tapGesture = UITapGestureRecognizer(
        target: self,
        action: #selector(imageTap(_:))
    )

    imageView.forEach { image in
        image.addGestureRecognizer(tapGesture)
        image.isUserInteractionEnabled = true
    }
}
@objc func imageTap(_ sender: UITapGestureRecognizer) {
    print("tapped")
}

수 많은 검색과 삽질을 통해 깨닫게 된 사실은 바로...

UITapGestureRecognizer 객체를 생성하고,

여러 개의 UIImageView에 객체를 할당해준 것이 문제였다.

 

여러 개의 뷰에 동일한 UITapGestureRecognizer 객체를 할당하면,

마지막으로 할당된 UIImageView에서만 제스처 인식이 동작한다.

 

이는 UITapGestureRecognizer 객체가 여러 UIImageView에 할당되었을 때,

UIImageView와의 결합이 끊어져서 발생하는 문제다.

 

따라서 여러 뷰에 대해 제스처를 추가할 때는,

각 뷰에 대해 별도의 UITapGestureRecognizer 객체를 생성하여 할당해주어야 한다.

해결

다음과 같이 UITapGestureRecognizer 를 forEach 스코프 안에 넣어주면 문제가 해결된다.

imageView.forEach { image in
    let tapGesture = UITapGestureRecognizer(
        target: self,
        action: #selector(imageTap(_:))
    )
    image.addGestureRecognizer(tapGesture)
    image.isUserInteractionEnabled = true
}

추가 의문

1. @IBOutlet을 생성해줄때 weak 수식어는 왜 사용하는가?

개별 UI 요소에 대한 @IBOutlet을 만들 때 Default값으로 'weak' 수식어를 일반적으로 사용한다.

그러면 ‘strong’ 수식어를 사용하면 안되는가?

결론은 문제가 발생할 수도 있고, 발생하지 않을 수도 있다.

 

VC가 메모리에서 정상적으로 해제된다면 문제가 발생하지 않지만

VC가 메모리에서 정삭적으로 해제되기 전에 View객체들이 메모리에서 해제되는 경우에는 문제가 발생할 수 있다.

 

예를 들어 앱을 사용하다가 메모리가 부족해지는 상황이 발생하게 되면,

ViewController는 didReceiveMemoryWarning()라는 인스턴스 메서드를 호출하는데,

이 메서드는 부족한 메모리를 확보하기 위해 VC 내부의 View를 nil 처리한다.

 

이 때 IBOutlet에 'strong'을 사용하면, View의 레퍼런스카운트는 줄었지만,

ViewController에서 View를 강하게 가리키고 있기 때문에 메모리 누수 문제가 발생할 가능성이 있다.

 

이러한 문제를 방지하기 위해 IBOutlet에는 일반적으로 'weak' 수식어가 사용된다.

2. IBOutlet Collection연결 시 왜 ‘weak’ 수식어를 사용하지 않는가?

@IBOutlet weak var imageView: UIImageView!
@IBOutlet var imageView: [UIImageView]!

IBOutlet Collection은 인터페이스 빌더(Interface Builder)에서 코드와 연결된 UI 요소들의 집합(배열 또는 집합)을 의미한다.

즉, Collection은 값 타입인 Struct다.

 

따라서, IBOutlet Collection은 값 타입이므로 참조하지 않으므로 강한 참조도 발생하지 않는다.

따라서 ‘weak’ 수식어를 사용할 수 없다.