[Swift] 여러 listCell 에서 customView accessory 를 사용할때 주의점

feat. Struct 안의 Reference property 는 복사되지 않는다

naljin
14 min readMar 17, 2023

TL; DR;

cell 간에는 동일한 하나의 custom view 를 사용 할 수 없음. custom view 를 사용하는 경우 별도의 row 마다 새 UICellAccessory 인스턴스를 만들어야 함.

들어가기 전에

UICellAccessory.customView 인스턴스를 만들고 여러 listCell 의 accessories 로 사용할때 hang 이 발생하는 문제점이 있었읍니다.. (차라리 에러 로그랑 크래시를 내주라..흑흑..)

원인 파악이 잘 안돼서 stack over flow 에 질문 후 답변을 받았는데여, 한글 기록용으로 이 글을 다 쓰고 나니 기본적인 내용을 놓친거 같아서 쩜 그렇긴 하지만,,?? 저 같은 실수를 하실 누군가를 위해 바칩니다…

그럼 ㄱㄱ

문제

나는 이런 UI 를 만들고 싶었다.

cell 우측에 있는 chevron down 이미지의 컴포넌트는 내가 custom 하게 만든 UICellAccessory 이다. 이제부터 이것을 moreAccessory 라고 부를 것이다.

그래서 나는 dataSourceredCellRegistration, blueCellRegistration 과 함께 아래와 같이 구성했다.

let redCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.text = "\(item)"
content.textProperties.color = .red
cell.contentConfiguration = content
let image = UIImageView(image: UIImage(systemName: "chevron.down"))
let configuration = UICellAccessory.CustomViewConfiguration(customView: image,
placement: .trailing(),
tintColor: .systemGray)
let moreAccessory = UICellAccessory.customView(configuration: configuration)
cell.accessories = [moreAccessory]
}

let blueCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.text = "\(item)"
content.textProperties.color = .blue
cell.contentConfiguration = content
let image = UIImageView(image: UIImage(systemName: "chevron.down"))
let configuration = UICellAccessory.CustomViewConfiguration(customView: image,
placement: .trailing(),
tintColor: .systemGray)
let moreAccessory = UICellAccessory.customView(configuration: configuration)
cell.accessories = [moreAccessory]
}

dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in

if indexPath.row.isMultiple(of: 2) {
return collectionView.dequeueConfiguredReusableCell(using: redCellRegistration, for: indexPath, item: identifier)
} else {
return collectionView.dequeueConfiguredReusableCell(using: blueCellRegistration, for: indexPath, item: identifier)
}
}

하지만 redCellRegistrationblueCellRegistration 안에서 각각 moreAccessory 를 만드는 중복코드가 있어서, 이 accessory 를 하나의 상수로 만들고, 이를 공통으로 사용하기로 했다.

let moreAccessory: UICellAccessory = {
let image = UIImageView(image: UIImage(systemName: "chevron.down"))
let configuration = UICellAccessory.CustomViewConfiguration(customView: image,
placement: .trailing(),
tintColor: .systemGray)
let accessory = UICellAccessory.customView(configuration: configuration)
return accessory
}()


let redCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.text = "\(item)"
content.textProperties.color = .red
cell.contentConfiguration = content
cell.accessories = [moreAccessory]
}

let blueCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.text = "\(item)"
content.textProperties.color = .blue
cell.contentConfiguration = content
cell.accessories = [moreAccessory]
}

하지만 위의 코드는 hang 을 야기했다.

Instruments 로 살펴보니 UICellAccessoryManager _updateAccessories:previousAccessories:withLayout:edge: 함수에서 너무 많은 weight 를 차지하고 있었다.

cpu 사용량도 100% 를 찍었고 메모리 사용량도 계속해서 올라갔다.

Trouble Shooting

moreAccessory 를 상수에 할당해서 사용하는 대신, moreAccessory를 반환하는 함수를 사용해보았다.

func moreAccessory() -> UICellAccessory {
let image = UIImageView(image: UIImage(systemName: "chevron.down"))
let configuration = UICellAccessory.CustomViewConfiguration(customView: image,
placement: .trailing(),
tintColor: .systemGray)
let accessory = UICellAccessory.customView(configuration: configuration)
return accessory
}

let redCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.text = "\(item)"
content.textProperties.color = .red
cell.contentConfiguration = content
cell.accessories = [moreAccessory()]
}

let blueCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.text = "\(item)"
content.textProperties.color = .blue
cell.contentConfiguration = content
cell.accessories = [moreAccessory()]
}

이 상황에서는 에러가 나지 않았다.

그래서 “음..동일한 인스턴스를 쓰는게 문제였나?” 라고 생각하며, moreAccessoryredCellRegistrationblueCellRegistration 안의 변수에 별도로 할당해보았다 (UICellAccessory 는 struct 이기 때문에 변수에 할당하면 인스턴스가 복사될 것이기 때문).


let moreAccessory: UICellAccessory = {
let image = UIImageView(image: UIImage(systemName: "chevron.down"))
let configuration = UICellAccessory.CustomViewConfiguration(customView: image,
placement: .trailing(),
tintColor: .systemGray)
let accessory = UICellAccessory.customView(configuration: configuration)
return accessory
}()

let redCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.text = "\(item)"
content.textProperties.color = .red
cell.contentConfiguration = content
let redCellAccessory = moreAccessory
cell.accessories = [redCellAccessory]
}

let blueCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.text = "\(item)"
content.textProperties.color = .blue
cell.contentConfiguration = content
let blueCellAccessory = moreAccessory
cell.accessories = [blueCellAccessory]
}

하지만 이때도 에러가 났다.

만약 내가 moreAccessory 를 customView 가 아닌 disclosureIndicator 같이 미리 정의된 accessory 를 사용하면 에러가 나지 않았다.

let moreAccessory: UICellAccessory = {
let accessory = UICellAccessory.disclosureIndicator()
return accessory
}()

이 에러 상황에 대한 실마리를 잡을 수 있는 내용이 있을까?

원인

UICellAccessory 가 struct 긴 하지만, custom accessory 를 만들 때 포함되는 customViewUIView class임.

UICellAccessory.CustomViewConfiguration 생성자

따라서 struct 가 복사 될 때, UIView 는 새롭게 생성되는 대신 동일한 인스턴스가 사용될 것. 하지만 cell 간에는 동일한 하나의 custom view (예시의 경우 UIImageView)를 사용 할 수 없음.

해결책은 custom view 를 사용하는 경우 별도의 row 마다 새 UICellAccessory 인스턴스를 만드는 것임.

따라서 맨 처음 각 registration 에서 UICellAccessory 인스턴스를 만드는 코드도 작동했고, UICellAccessory를 반환하는 함수는 호출될 때마다 새 인스턴스를 만들기 때문에 작동했던 것.

마무리

원인을 알고나면 이걸 왜 놓쳤지? 싶지만, 그 전에는 도저히 모르겠었던.. ㅎㅎ 이제 비슷한 이슈 나오면 좀 더 빠르게 갈피를 잡을 수 있겠져 뭐… 이러면서 진화하는거 아니겠서여~~

혹시 아래 내용을 읽으면서

UICellAccessory 가 struct 긴 하지만, custom accessory 를 만들 때 포함되는 customViewUIView class임. 따라서 struct 가 복사 될 때, UIView 는 새롭게 생성되는 대신 동일한 인스턴스가 사용될 것.

🤔 : struct 안에 class 가 있을때 복사가 아니라 참조로 작동된다고??

라면서 잘 이해가 안되시는 분이 계시다면 아래 글을 참고해주세요

예전에 읽고 좋았어서 언제 한번 정리해야지~ 하고 냅다 링크만 첨부해놨었는데.. 이렇게 링크라도 남겨두는거져 뭐..

ㄴㅔ?? 삼년전이여?

그럼 20000!

출처

--

--

Responses (2)