티스토리 뷰
기존 프로젝트를 Tuist로 모듈화한 후 마주쳤던 문제에 대해서 간단하게 포스팅해보려고 합니다.
- 프로젝트 스펙: UIKit, SnapKit, Tuist, RxCocoa, RxSwift
문제상황
빌드시 SnapKit 라이브러리에서 오류가 발생했습니다.
Thread 1: EXC_BAD_ACCESS(code=1, address=0x0)
해당 오류가 발생한 지점은
이렇게 단순히 Cell 내부에서 Contraints를 설정하는 부분이었습니다.
⬇️ 전체 코드
class RegisteredPetCell: BaseCollectionViewCell {
// MARK: Constant
private let IMAGE_SIZE: CGFloat = 70
// MARK: UI Component
private let petImageView: UIImageView = .init(imageName: "cat-sample")
private let petNameLabel: UILabel = .init()
private let petInfoLabel: UILabel = .init()
let detailButton: UIButton = .init(image: UIImage(systemName: "arrow.forward")!)
// MARK: Set Methods
override func setStyle() {
addShadowWithRoundedCorners()
layer.borderColor = UIColor.grayScale200.cgColor
layer.borderWidth = 1
petImageView.layer.cornerRadius = IMAGE_SIZE / 2
petNameLabel.setDefaultFont(size: 18, weight: .bold)
petNameLabel.textColor = .black
petInfoLabel.setDefaultFont(size: 16, weight: .regular)
petInfoLabel.textColor = .grayScale600
detailButton.imageView?.tintColor = .black
}
override func setHierarchy() {
[petImageView, petNameLabel, petInfoLabel, detailButton]
.forEach { addSubview($0)}
}
override func setLayout() {
petImageView.snp.makeConstraints {
$0.leading.equalToSuperview().offset(10)
$0.centerY.equalToSuperview()
$0.width.height.equalTo(IMAGE_SIZE)
}
petNameLabel.snp.makeConstraints {
$0.leading.equalTo(petImageView.snp.trailing).offset(15)
$0.centerY.equalToSuperview().offset(-10)
}
petInfoLabel.snp.makeConstraints {
$0.leading.equalTo(petImageView.snp.trailing).offset(15)
$0.centerY.equalToSuperview().offset(12)
}
detailButton.snp.makeConstraints {
$0.trailing.equalToSuperview().inset(10)
$0.width.height.equalTo(20)
$0.centerY.equalToSuperview()
}
}
}
문제 분석
우선 Thread 1: EXC_BAD_ACCESS(code=1, address=0x0) 만으로 알 수 있는 정보는 거의 없기 때문에.. 오류의 시작점인 snp.makeContraints 부분부터 디버깅하며 인스턴스를 추적했습니다.
오류가 발생하는 SanpKit 부분을 보면 item에 관련한 오류인 것을 알 수 있습니다.
여기서 item은 snp.makeContraints를 호출한 객체로, contraints를 설정받을 객체입니다.
디버깅을 하다보니 CollectionView의 cell 내부 요소의 참조주소가 모두 0인 것을 확인할 수 있었습니다.
즉, UIImageView, UILabel 등 UIKit 요소가 올바르게 초기화되지 않아서 Snapkit이 제약조건을 설정하는데 실패한 것입니다.
해결 방안
문제는 접근제어자였습니다.
해당 프로젝트에서는 코드의 일관성과 필수 초기화 메서드 생략을 위해 아래와 같이 BaseCollectionViewCell을 구성해두고 이를 UICollectionViewCell대신 상속해서 사용했습니다.
class BaseCollectionViewCell: UICollectionViewCell, BaseViewProtocol {
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
setStyle()
setHierarchy()
setLayout()
setBind()
}
override func prepareForReuse() {
super.prepareForReuse()
}
func setStyle() { }
func setHierarchy() { }
func setLayout() { }
func setBind() { }
}
그리고 Tuist 구성중에 Base Module과 UI Module을 분리하게 되면서 아래와 같은 구조가 되었습니다.
그리고 이에 맞게 BaseCollectionViewCell의 접근제어자를 다음과 같이 변경했고, 이것이 오류의 원인이었습니다.
open class BaseCollectionViewCell: UICollectionViewCell, BaseViewProtocol {
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
setStyle()
setHierarchy()
setLayout()
setBind()
}
public override func prepareForReuse() {
super.prepareForReuse()
}
open func setStyle() { }
open func setHierarchy() { }
open func setLayout() { }
open func setBind() { }
}
바로 init(frame:)메서드의 접근제어자를 수정해주지 않은 것이 원인이었습니다.
결론적으로 위 코드에서 init(frame: CGRect) 의 접근제어자 또한 public으로 바꿔주어 해결할 수 있었습니다.
public override init(frame: CGRect) {
super.init(frame: frame)
setStyle()
setHierarchy()
setLayout()
setBind()
}
자세히
UIView를 코드로 구성하기 위해선 init?(coder: NSCoder)와 init(frame: CGRect) 두가지 메서드를 구현해줘야하는데요,
init(frame: CGRect)의 접근제어자가 internal인 상태에서 UI Module에서 타 모듈에 있는 슈퍼클래스의 init(frame: CGRect)를 올바르게 상속받지 못해 발생한 메모리 참조 오류였습니다.
더 자세하게 콜스택을 보면 internal로 init(frame:)을 설정했던 경우에는 collection view에서 dequeue가 된 후 바로 슈퍼클래스인 BaseCollectionView의 초기화 메서드가 실행되는 것을 볼 수 있고,
public으로 설정한 경우에는 올바르게 서브클래스인 RegisteredPetCell이 BaseCollectionView의 초기화 메서드를 호출하고 있는 것을 볼 수 있습니다.
결론
원래 슈퍼클래스의 함수는 서브클래스에서 접근(호출, 오버라이딩)하지 않는다면 internal이어도 문제가 없습니다. 하지만 UIView의 특성상 특별한 생성자-init(frame:)-가 필요했고, 슈퍼클래스에서 이 생성자를 정의했으나 접근제어자로 인해 상속받지 못해 올바른 초기화에 실패해 생긴 오류였습니다.
public을 붙이지 않아도 컴파일이 되고, init(frame: CGRect)메서드 또한 호출이 되었기 때문에 문제의 원인을 찾는데 오래걸렸습니다.
심지어 올바르게 초기화되지 않은 UIKit 컴포넌트에 접근하는 것(label.textColor = .black 등)도 가능했습니다...
RxCocoa의 문제인지, Tuist를 설정하면서 디팬던시 버전에 문제가 생긴건지 여러방면으로 고민도 해보았는데 사소한 실수였네요 ㅎㅎ
읽어주셔서 감사합니다.
'iOS > UIKit' 카테고리의 다른 글
[iOS] PageViewController 직접 구현하기 + UIPageViewController 오류 (2) | 2024.07.13 |
---|---|
[iOS] 코디네이터 패턴으로 TabBar 만들기 (UIKit TabBar 코드로 만들기) (2) | 2023.01.19 |
- Total
- Today
- Yesterday
- reactive programming
- 프로그래머스
- programmers
- coordinator pattern
- MVC
- Swift Concurrency
- design pattern
- MVVM
- Flux
- swift
- 리액티브 프로그래밍
- notion
- TestCode
- 노션
- Bloking/Non-bloking
- Flutter
- 아키텍쳐 패턴
- ios
- GetX
- combine
- DocC
- SwiftUI
- 코디네이터 패턴
- MVI
- SWM
- Architecture Pattern
- 소프트웨어마에스트로
- healthkit
- RX
- 비동기/동기
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |