[iOS] UICellAccessory 종류 알아보기

탑꾸? 🤷🏻‍♀️ 아니 셀꾸요 🙆🏻‍♀️

naljin
39 min readJan 21, 2023

들어가기 전에

세상엔 셀을 꾸밀 수 있는~~ 멋진 액세서리들이 많아여~~~ 셀 꾸미기~ 셀꾸~

위 캡쳐에서 ⛺️ 빼고 다 iOS 에서 기본적으로 제공하는 cell 액세서리인데여, 걍 이런 식으로 추가하면 됨요! 멋지져?!

cell.accessories = [ 
.checkmark(),
.delete(),
.reorder()
]

🤔 : 엥? 난 cell 에 accessories 프로퍼티가 있는거 처음 보는데?

후후 이 cell 은 사실 UICollectionViewListCell 입니다. 이 타입은 cell 을 꾸밀 수 있는 accessories 프로퍼티를 가지고 있져

오늘 글에서는 UICollectionViewListCell 이 무엇이냐! 를 살펴보진 않고,, 그저 accessories 안에 들어갈 수 있는 UICellAccessory 는 무엇이고 어떤 종류들이 있냐! 를 알아볼겁니다.

그럼 ㄱㄱ~~!

UICellAccessory

UICellAccessoryUICollectionViewListCell 에 추가할 수 있는 시각적 요소로, iOS 14.0 부터 사용 가능합니다.

UIKit 은 표준 시스템 cell accessory 들을 정의해놨는데요! 시스템 액세서리의 위치는 미리 정의되어 있습니다. 시스템은 렌더링 순서와 셀의 어느 쪽에 표시되는지를 자동으로 결정합니다. array 에 있는 시스템 액세서리의 순서는 실제 배치 순서에 영향을 주지 않습니다.

이렇게 미리 정의된 액세서리의 모양, 위치(leading / trailing) 등은 변경할 수 없지만 색상과 같은 일부 값은 커스텀 할 수도 있습니다.

만약 한개 이상의 동일한 시스템 accessory 를 추가한다면

cell.accessories = [
.delete(),
.delete(),
]

system 은 이렇게 exception 을 throw 합니다.

Thread 1: "Accessories array contains more than one system accessory of the same type. Duplicate accessories: <UICellAccessoryDelete: 0x600002ab6080> <UICellAccessoryDelete: 0x600002ab60c0>"

흠냐리 바로 코드로 볼까요?

전체 코드는 Implementing Modern Collection ViewsReorderableListViewController 를 대충 수정했습니다.

let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Emoji> { (cell, indexPath, emoji) in
var contentConfiguration = UIListContentConfiguration.valueCell()
contentConfiguration.text = emoji.text
cell.contentConfiguration = contentConfiguration
}

이렇게 표시되는 cell 이 있구여?

이제 cell 에 모든 액세서리를 냅다 추가해보겠습니다.

let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Emoji> { (cell, indexPath, emoji) in
var contentConfiguration = UIListContentConfiguration.valueCell()
contentConfiguration.text = emoji.text
cell.contentConfiguration = contentConfiguration
cell.accessories = [
.outlineDisclosure(displayed: .always),
.disclosureIndicator(displayed: .always),
.delete(displayed: .always),
.reorder(displayed: .always),
.checkmark(displayed: .always),
.insert(displayed: .always),
.multiselect(displayed: .always),
.label(text: "naljin", displayed: .always),
.detail( displayed: .always),
.popUpMenu(
UIMenu(children: [
UIAction(title: "첫번째", handler: { _ in }),
UIAction(title: "두번째", handler: { _ in })
]),
displayed: .always)
]
}

홀리 몰리 뭔가 많이 생겼네요~~~

아까 “array 에 있는 시스템 액세서리의 순서실제 배치 순서영향을 주지 않습니다.” 라고 했져? 실제로 배열의 첫번째에는 outlineDisclosure 가 설정되어있지만, UI 상으로 가장 먼저 보이는건 delete 액세서리네여.

그럼 하나씩 살펴보러 갑시다 ㄱㄱ

delete

위치 : leading

모양 : 빨간색 circle 안에 minus 사인이 있음

정의

static func delete(
displayed: UICellAccessory.DisplayedState = .whenEditing,
options: UICellAccessory.DeleteOptions = DeleteOptions(),
actionHandler: UICellAccessory.ActionHandler? = nil
) -> UICellAccessory

흠.. 생소한 파라미터들이 있네요.. 함 보자고요

displayed

accessory 가 표시되는 상태(조건)를 설명합니다.

displayed의 타입인 UICellAccessory.DisplayedState 은 enum 으로, 아래와 같은 세가지 case 가 있습니다.

  • always - accessory 가 언제나 표시됩니다.
  • whenEditing - cell 이 editing mode 일때만 accessory 가 표시됩니다.
  • whenNotEditing - cell 이 editing mode 가 아닐때만 accessory 가 표시됩니다.

그래서

cell.accessories = [.delete()]

처럼 설정하면 기본적으로 delete 액세서리는 안보임여. 왜냐면 delete accessory 의 displayed 기본 값은 whenEditing 이니까요.

collectionView.isEditing = true 처럼 editing mode 일때는 보이겠져?

만약 editing mode 와 관계없이 무조건 보이게하고 싶으면, .delete(displayed: .always) 로 만들면 됩니다.

options

자자 두번째 파라미터인 delete accessory 를 위한 설정 옵션입니다.

어떤 옵션이 가능한지 UICellAccessory.DeleteOptions 를 살펴보면

UICellAccessory.DeleteOptions.init(
isHidden: Bool? = nil,
reservedLayoutWidth: UICellAccessory.LayoutDimension? = nil,
tintColor: UIColor? = nil,
backgroundColor: UIColor? = nil
)

tintColorbackgroundColor 는 대충 감이올텐데여, 각각 빨강, 초록색으로 설정하고 돌려봅시다

UICellAccessory.delete(displayed: .always,
options: .init(tintColor: .red,
backgroundColor: .green))

예상대로 나왔나여?

그럼 isHidden 으로 넘어가봅시다.

🤔 : 액세서리의 hidden 여부는 이미 displayed 에서 설정된거 아님?

과연 그럴까여? displayed: .always 로 설정한 상태에서 isHiddentrue 로 설정해봅시다.

UICellAccessory.delete(displayed: .always,
options: .init(isHidden: true))

두둥 액세서리의 영역 자체는 잡히는데 해당 뷰가 보이지는 않네여. ㅇㅋㅇㅋ isHidden 뭔말인지 이해!

이제 옵션에서 마지막으로 남은 reservedLayoutWidth 으로 넘어가볼게여.

정의에는 “시스템이 accessory 를 위해 지정한 너비. 이 가운데에 accessory 를 배치함” 이라고 나와있어여

이 속성을 사용하면 액세서리의 크기가 다양한 경우에도, 시스템 및 커스텀 액세서리를 일관되게 수평으로 정렬할 수 있습니다.

무슨 말인지 모르겠져? 그림으로 살펴봅시다

여기에는 세 개의 서로 다른 셀이 있습니다. 각 셀은 이미지와 텍스트가 있네여.

여기서 각 셀들이 약간 다른 width 를 가진 이미지를 사용한다면 cell 사이의 정렬이 깨집니다.

이런 현상이 발생하는 이유는 이미지가 leading 으로 정렬된 상태에서, 텍스트가 각 이미지의 trailing edge 에서 동일한 padding 으로 배치되기 때문입니다.

이러한 cell 사이에서 올바른 정렬을 위해서는 각 이미지에 대해 reserved layout size 를 지정해야 합니다. 각 configuration 에서 이미지에 대해 동일한 reserved layout width 를 설정하는 경우, 예약된 공간(reserved amount of space) 내에서 이미지를 수평으로 가운데에 배치합니다.

아래 빨간색 점선 사이의 거리가 각 이미지에 대한 reserved layout width 입니다.

reserved layout size 는 이미지의 실제 크기에 영향을 주지 않습니다. 만약 이미지가 reserved layout size 보다 크면 해당 영역 밖으로 나갈 수 있습니다.

이미지에 reserved layout size 를 사용하면 text 도 올바르게 정렬되는데요, 이는 텍스트가 reserved layout 영역에 상대적으로 위치하기 때문입니다.

symbol image 를 사용하는 경우, UIKit는 standard reserved layout size 를 자동으로 적용합니다. non-symbol 이미지에 대해서는 필요한 경우 수동으로 요청할 수 있습니다.

UICellAccessory.LayoutDimension 타입은 enum 으로 아래의 세가지 case 가 있습니다.

  • actual - 액세서리 레이아웃은 실제 치수(dimension)를 사용
  • standard - 액세서리 레이아웃은 액세서리에 대한 시스템 표준 레이아웃 dimension 를 사용
  • custom(CGFloat) - 액세서리 레이아웃은 사용자가 지정한 값을 사용

아래 그림을 보면 족굼 감이 오실까여? 저도 딱 여러분들이 이해한만큼 이해한 상태임다.

actionHandler

사용자가 delete accessory 와 상호 작용 (클릭이죠 뭐..?) 할때 호출할 클로저입니다. 타입이 UICellAccessory.ActionHandler 이긴 한데 그냥 아래와 같은 typealias 예여.

typealias UICellAccessory.ActionHandler = () -> Void

그래서 결론적으로 요런 식으로 모든 인자를 넘겨서 삭제 액세서리를 생성할 수도 있습니다.

cell.accessories = [
.delete(displayed: .always,
options: .init(isHidden: false,
reservedLayoutWidth: .standard,
tintColor: .red,
backgroundColor: .green),
actionHandler: {
print("삭제 버튼 클릭됨")
})
]

이제 다음 액세서리로 넘어가보져! 대충 중요한 개념들은 다 봤으니 이 다음부터는 쭉쭉 넘어갈 수 있을거예요!!

insert

위치 : leading

모양 : 초록색 circle 안에 plus 사인이 있음

정의

static func insert(
displayed: UICellAccessory.DisplayedState = .whenEditing,
options: UICellAccessory.InsertOptions = InsertOptions(),
actionHandler: UICellAccessory.ActionHandler? = nil
) -> UICellAccessory

위에서 본 delete 액세서리와 매우 유사합니다.

얘도 displayed 의 기본 값이 whenEditing 이라서 editing mode 가 아니면 기본적으로 노출되지 않습니다.

options 타입은 UICellAccessory.InsertOptions 인데 초기화 인자들을 보면 똑같이 생김여

UICellAccessory.InsertOptions.init(
isHidden: Bool?,
reservedLayoutWidth: UICellAccessory.LayoutDimension?,
tintColor: UIColor?,
backgroundColor: UIColor?
)

ㅋㅋ 개껌이쥬? 다음

multiselect

위치 : leading

모양 : cell 의 선택 상태에 따라 달라짐. unselected 일때는 빈 circle 이고, selected 일때는 checkmark 로 표시된 채워진 circle.

만약 collectionView.allowsMultipleSelection 이 기본값인 false 로 되어있으면 한개만 선택이 가능하고

collectionView.allowsMultipleSelection = true 로 설정되어있으면 여러개 선택 할 수 있습니다.

정의

static func multiselect(
displayed: UICellAccessory.DisplayedState = .whenEditing,
options: UICellAccessory.MultiselectOptions = MultiselectOptions()
) -> UICellAccessory

얘도 displayed 의 기본 값이 whenEditing 이네여

그래서 사실

cell.accessories = [.multiselect()]

로 생성하면 collectionView.isEditing = true 처럼 editing mode 일때만 보여요.

근데 문제는 이렇게 하면 선택이 안먹는거예여?

왜 그러냐 넌 또.. 하고 찾아본 결과 collectionView 의 allowsSelectionDuringEditingtrue 로 줘야하더라구염. 그럼 한개씩 선택 가넝합니다.

collectionView.allowsSelectionDuringEditing = true

만약 allowsMultipleSelectionDuringEditingtrue 로 주면 여러 개도 선택 가넝해여!

collectionView.allowsMultipleSelectionDuringEditing = true

자 다시 돌아와서 저희 multiselect 보고 있었져?

static func multiselect(
displayed: UICellAccessory.DisplayedState = .whenEditing,
options: UICellAccessory.MultiselectOptions = MultiselectOptions()
) -> UICellAccessory

multiselectoptions 파라미터는 UICellAccessory.MultiselectOptions 타입인데, 다른 액세서리에서 살펴본것과 다를 것 없슴다

UICellAccessory.MultiselectOptions.init(
isHidden: Bool?,
reservedLayoutWidth: UICellAccessory.LayoutDimension?,
tintColor: UIColor?,
backgroundColor: UIColor?
)

label

위치 : trailing

모양 : 내가 설정한 text (항목의 개수를 표시하는 것과 같이 짧은 텍스트를 표시하는데 사용)

정의

static func label(
text: String,
displayed: UICellAccessory.DisplayedState = .always,
options: UICellAccessory.LabelOptions = LabelOptions()
) -> UICellAccessory

text 는 라벨에 표시할 text 고, displayed 의 기본 값은 always 네여

options 의 타입은 UICellAccessory.LabelOptions 입니다.

UICellAccessory.LabelOptions.init(
isHidden: Bool? = nil,
reservedLayoutWidth: UICellAccessory.LayoutDimension? = nil,
tintColor: UIColor? = nil,
font: UIFont? = nil,
adjustsFontForContentSizeCategory: Bool? = nil
)

오 근데 여기는 못 보던 파라미터들이 있네요? font 는 뭐... 그냥 폰트일테니까 넘어가고!

adjustsFontForContentSizeCategory 는 content size category 에 따라 label 이 자동으로 font 를 조정하는지 여부를 결정하는 bool 값입니다. 그러니까 유저가 시스템에서 글꼴을 크게 변경하면 이 label 액세서리의 폰트도 크게 변경되냐!? 를 지정하는 값이겠졍?? 기본값은 true 래여.

checkmark

위치 : trailing

모양 : 기본 시스템 green 색상의 체크마크입니다.

아니 근데 설명은 이렇게 green 색상이라고 써있는데 누가봐도 파란색 아니예요..?? 제가 아는 green 이 아닌…?? 아시는 분 있으면 알려주십셔 plz

재준아 너는 모르잖아 초록색..?

정의

static func checkmark(
displayed: UICellAccessory.DisplayedState = .always,
options: UICellAccessory.CheckmarkOptions = CheckmarkOptions()
) -> UICellAccessory

displayed 의 기본 값은 always 네요.

options 타입은 UICellAccessory.CheckmarkOptions 인데 얘도 뭐 새로울건 없군요

UICellAccessory.CheckmarkOptions.init(
isHidden: Bool?,
reservedLayoutWidth: UICellAccessory.LayoutDimension?,
tintColor: UIColor?
)

detail

위치 : trailing

모양 : 시스템 information 버튼

(근데 이름이 detail 이라고 하면 > 모양이 생각나지 않으세여? 저만 그런가.. 그냥 information 이라고 하지.. )

정의

static func detail(
displayed: UICellAccessory.DisplayedState = .always,
options: UICellAccessory.DetailOptions = DetailOptions(),
actionHandler: UICellAccessory.ActionHandler? = nil
) -> UICellAccessory

displayed 의 기본 값은 always 입니다.

options 의 타입은 UICellAccessory.DetailOptions 입니다.

UICellAccessory.DetailOptions.init(
isHidden: Bool?,
reservedLayoutWidth: UICellAccessory.LayoutDimension?,
tintColor: UIColor?
)

뻔해. 다음. 하고 넘어가기 전에 detail 액세서리는 최소 버전 15.4 부터 사용할 수 있다는 점이 특이 포인트입니다. 다른 애들이랑 똑같이 14.0 으로 맞추지.. 흥 제법 웃겨

popUpMenu

위치 : trailing

모양 : 위쪽과 아래쪽을 가리키는 한 쌍의 chevron.

정의

static func popUpMenu(
_ menu: UIMenu,
displayed: UICellAccessory.DisplayedState = .always,
options: UICellAccessory.PopUpMenuOptions = PopUpMenuOptions(),
selectedElementDidChangeHandler: UICellAccessory.MenuSelectedElementDidChangeHandler? = nil
) -> UICellAccessory

첫번째 menu 파라미터에는 아래와 같이 UIMenu 를 넣어주면 됩니다.

UICellAccessory.popUpMenu(
UIMenu(children: [
UIAction(title: "첫번째", handler: { action in print("첫번째 클릭")}),
UIAction(title: "두번째", handler: { action in print("두번째 클릭")})
])
)

displayed 기본 값은 always 입니다.

options 의 타입은 UICellAccessory.PopUpMenuOptions 입니다.

UICellAccessory.PopUpMenuOptions.init(
isHidden: Bool?,
reservedLayoutWidth: UICellAccessory.LayoutDimension?,
tintColor: UIColor?
)

그리고 마지막으로 넘겨 받는 selectedElementDidChangeHandler 는 menu 에서 이전에 선택한 것과 다른 element 를 선택할 때 호출하는 클로저입니다.

typealias UICellAccessory.MenuSelectedElementDidChangeHandler = (UIMenu) -> Void

근데 popUpMenu 나 아까 본 multiselect 나 둘 다 셀의 아무 곳이나 클릭해도 동작하는 애들이란 말이죠?

그럼 아래와 같이 두 액세서리 모두 설정을 한다면 어떻게 될까요?

cell.accessories = [
.multiselect(displayed: .always),
.popUpMenu(
UIMenu(children: [
UIAction(title: "첫번째", handler: { action in print("첫번째 클릭")}),
UIAction(title: "두번째", handler: { action in print("두번째 클릭")})
])
)
]

결과적으로 popUpMenu 의 동작이 우선시 되고 multiSelect 는 동작하지 않습니다. 이는 즉 didSelectItemAt delegate 함수도 호출되지 않는다는 뜻이겠져.

이를 방지하기 위해서는… 한 10분 정도 구글링했을때는 안나오네여..? 미래의 제가 쓸일이 있으면 더 고생해주겠져 뭐.. ㅎㅎ ㅎ오늘도 미루는 삶!

아 팝업 메뉴 액세서리는 최소 버전 16 부터 사용할 수 있습니다.

disclosureIndicator

위치 : trailing

모양 : trailing 방향을 가리키는 disclosure chevron

정의

static func disclosureIndicator(
displayed: UICellAccessory.DisplayedState = .always,
options: UICellAccessory.DisclosureIndicatorOptions = DisclosureIndicatorOptions()
) -> UICellAccessory

displayed 의 기본 값은 always 입니다.

options 의 타입은 UICellAccessory.DisclosureIndicatorOptions 입니다.

UICellAccessory.DisclosureIndicatorOptions.init(
isHidden: Bool?,
reservedLayoutWidth: UICellAccessory.LayoutDimension?,
tintColor: UIColor?
)

outlineDisclosure

위치 : iOS 나 Mac Catalyst 의 헤더 — trailing, Mac Catalyst 의 cell — leading

모양 : disclosure 액세서리와 비슷하게 생겼지만 클릭시 회전됩니다.

맨 첫번째 셀을 주목해주세요

만약 child item 을 넣어주면 이렇게 cell 이 확장 및 축소 되는것을 볼 수 있습니다.

만약 이 액세서리가 없으면 expand 가능한 child item 이 있어도 cell 확장이 되지 않습니다. 즉 cell 이 확장 / 축소 가능하다! 를 나타낼 (indicate) 뿐만 아니라, 그 기능 자체도 가능 (enable) 토록 하는 액세서리라고 할 수 있습니다.

Use this cell accessory to indicate that an item can expand and collapse, and to enable the user to toggle between the expanded and collapsed states.

정의

static func outlineDisclosure(
displayed: UICellAccessory.DisplayedState = .always,
options: UICellAccessory.OutlineDisclosureOptions = OutlineDisclosureOptions(),
actionHandler: UICellAccessory.ActionHandler? = nil
) -> UICellAccessory

displayed 의 기본 값은 always 입니다.

options 의 타입은 UICellAccessory.DisclosureIndicatorOptions 입니다.

UICellAccessory.DisclosureIndicatorOptions.init(
style: UICellAccessory.OutlineDisclosureOptions.Style?,
isHidden: Bool?,
reservedLayoutWidth: UICellAccessory.LayoutDimension?,
tintColor: UIColor?
)

오호.. 여기엔 처음 보는 style 이라는 파라미터가 있네여. 이름대로 outline disclosure 의 스타일을 나타낸다고 합니다.

style 의 타입은 UICellAccessory.OutlineDisclosureOptions.Style enum 인데 아래와 같은 3가지 case 가 있습니다.

  • cell - 중첩된 children 이 있는 선택 가능한 셀에 사용할 스타일. 이 스타일을 사용하면 outline disclosure 액세서리를 클릭하는 경우에만 expansion 상태가 변경됨. cell 자체를 클릭하면 item 에 대한 선택이 됨.
  • header - 섹션 헤더에 사용할 스타일. 이 스타일을 사용하면 cell 의 어느 곳을 클릭하든 expansion 상태가 변경됨. 따라서 이 스타일을 사용하면 cell 자체는 selectable 하지 않게 됨.
  • automatic - 셀의 configuration 이 섹션 헤더인지 여부에 따라 시스템이 자동으로 스타일을 결정

reorder

위치 : trailing

모양 : 기본 system gray 색상의 세 개의 수평선

컬렉션 뷰에서 순서 변경을 지원하는 경우, 사용자는 이 액세서리를 기준으로 셀을 드래그해서 순서를 변경할 수 있습니다.

그러려면 우선 reorderingHandlers 부터 잘 지정해줘야겠져?

dataSource.reorderingHandlers.canReorderItem = { item in return true }
dataSource.reorderingHandlers.didReorder = { transaction in
// 구현
}

정의

static func reorder(
displayed: UICellAccessory.DisplayedState = .whenEditing,
options: UICellAccessory.ReorderOptions = ReorderOptions()
) -> UICellAccessory

얘는 displayed 의 기본 값이 whenEditing 이네여.

options 타입은 UICellAccessory.ReorderOptions 입니다.

UICellAccessory.ReorderOptions.init(
isHidden: Bool?,
reservedLayoutWidth: UICellAccessory.LayoutDimension?,
tintColor: UIColor?,
showsVerticalSeparator: Bool?
)

showsVerticalSeparator 라는 새로운 파라미터가 있네여. 다른 액세서리가 앞에 있을때 그 사이에 vertical separator 표시할건지를 결정하는 값이라고 합니다. 기본 값은 true 래여

만약 자신 앞에 다른 액세서리가 없으면 vertical separator 가 없지만

임의로 checkmark 액세서리를 추가해보면? 요렇게 선이 생깁니다.

만약 showsVerticalSeparator 값을 false 로 주면 앞에 다른 액세서리가 있어도 separator 는 없어지겠져?

cell.accessories = [
.checkmark(),
.reorder(displayed: .always,
options: .init(showsVerticalSeparator: false)
)
]

custom

지금까지는 시스템에서 정의된 액세서리를 살펴봤는데요! custom view 를 이용한 accessory 를 만들수도 있습니다!

바로 이런식으로 말이져

let myView = UIView(frame: CGRect(origin: .zero,
size: CGSize(width: 10, height: 10)))
myView.backgroundColor = .red
cell.accessories = [
.customView(configuration: .init(customView: myView,
placement: .leading()))
]

보다시피 customView 라는 UICellAccessory 타입 메서드를 사용했는데요!

static func customView(configuration: UICellAccessory.CustomViewConfiguration) -> UICellAccessory

파라미터로 받는 configuration 의 타입을 살펴볼까요?

UICellAccessory.CustomViewConfiguration 의 초기화 함수는 아래와 같이 생겼습니다.

init(customView: UIView, 
placement: UICellAccessory.Placement,
isHidden: Bool?,
reservedLayoutWidth: UICellAccessory.LayoutDimension?,
tintColor: UIColor?,
maintainsFixedSize: Bool?)

받는게 꽤 많네요. 그 중 다른 액세서리에서 못 본 파라미터들만 살펴보자면, 우선 customView 는 내가 액세서리로 나타낼 뷰입니다.

placement 파라미터는 액세서리의 위치를 결정합니다. UICellAccessory.Placement 타입은 enum 으로, 아래 두 케이스를 이용해서 cell 의 leading 혹은 trailing 에 액세서리를 배치할 수 있습니다.

case leading(displayed: UICellAccessory.DisplayedState, at: UICellAccessory.Placement.Position)
case trailing(displayed: UICellAccessory.DisplayedState, at: UICellAccessory.Placement.Position)

그러니까 간단하게 쓰려면 이런식으로 쓸 수 있단말이져

UICellAccessory.customView(configuration: .init(customView: myView,
placement: .leading()))

하지만 각 case 에서 associated value 로 받는 값들이 있으니까 함 볼까여? leading 부터 가봅시다.

case leading(
displayed: UICellAccessory.DisplayedState = .always,
at: UICellAccessory.Placement.Position = { $0.count }
)

displayed 의 기본값은 always 군여.

at 으로는 UICellAccessory.Placement.Position 을 받고 있네여. 이 타입은 다른 액세서리에 대한 인덱스 위치를 나타냅니다.

typealias UICellAccessory.Placement.Position = (_ accessories: [UICellAccessory]) -> Int

leading 에서 at 의 기본 값은 { $0.count } 로 설정이 되어있으니까 leading 에 있는 액세서리 중 가장 마지막에 위치하겠군여

실제로 leading 에 위치하는 액세서리인 insertmultiselect 를 같이 배치했을 때 customView 는 가장 마지막에 위치합니다.

cell.accessories = [
.insert(displayed: .always),
.customView(configuration: .init(customView: myView,
placement: .leading())),
.multiselect(displayed:.always)

]

그러면 UICellAccessory.Placement 의 다른 case 인 trailing 으로 넘어가볼게여.

case trailing(
displayed: UICellAccessory.DisplayedState = .always,
at: UICellAccessory.Placement.Position = { _ in 0 }
)

trailing 에서 at 의 기본 값은 { _ in 0 } 로 설정이 되어있으니까 trailing 에 있는 액세서리 중 가장 첫번째에 위치하겠군여

실제로 trailing 액세서리인 checkmarkreorder 를 같이 배치했을 때 customView 는 가장 첫번째에 위치합니다.

cell.accessories = [
.checkmark(),
.customView(configuration: .init(customView: myView,
placement: .trailing())),
.reorder(displayed: .always)
]

만약 custom 액세서리의 leading / trailing 을 정하는 것에 더해서, 특정 액세서리 앞 / 뒤인지까지 지정하고 싶어!! 라고 한다면 UICellAccessory.Placement 의 두 타입 메서드를 이용할 수도 있습니다

static func position(after: UICellAccessory) -> UICellAccessory.Placement.Position
static func position(before: UICellAccessory) -> UICellAccessory.Placement.Position

이런 식으로 말이져

UICellAccessory.customView(configuration: .init(customView: myView,
placement: .leading(at: UICellAccessory.Placement.position(after: .insert()))))

그래서 실제로 insertdelete 액세서리를 추가하고, custom 액세서리를 delete 뒤에 넣겠다! 라고 지정을 해주면

cell.accessories = [
.insert(displayed: .always),
.delete(displayed: .always),
.customView(configuration: .init(customView: myView,
placement: .leading(at: UICellAccessory.Placement.position(after: .delete()))))
]

요렇게 원하는대로 잘 나옵니다.

그런데 만약 custom 액세서리를 특정 액세서리 뒤에 위치 시킬거임!! 하고 position(after:) 를 사용했는데, 이때 "특정 액세서리" 가 cell 에 추가되지 않은 상태라면 어떻게 될까요?

아래처럼 코드처럼 지금 액세서리로 세팅이 안된 multiselect 뒤에 custom 액세서리를 설정하겠다! 라고 하는 상황처럼 말이져

cell.accessories = [
.insert(displayed: .always),
.delete(displayed: .always),
.customView(configuration: .init(customView: myView,
placement: .leading(at: UICellAccessory.Placement.position(after: .multiselect()))))
]

이럴때 position(after:) 는 custom accessory 를 가장 뒤에 위치시킵니다.

반대로 position(before:) 는 인자로 넘긴 액세서리가 없을때 custom accessory 를 가장 앞에 위치시킵니다.

cell.accessories = [
.insert(displayed: .always),
.delete(displayed: .always),
.customView(configuration: .init(customView: myView,
placement: .leading(at: UICellAccessory.Placement.position(before: .multiselect()))))
]

또 다른 상황으로 insert, delete, checkmark 액세서리를 추가하고, 커스텀 액세서리의 placementleading, positioncheckmark 액세서리 뒤로 하겠다! 라고 해볼게요

cell.accessories = [
.insert(displayed: .always),
.delete(displayed: .always),
.checkmark(displayed: .always),
.customView(configuration: .init(customView: myView,
placement: .leading(at: UICellAccessory.Placement.position(after: .checkmark()))))
]

근데 여러분.. checkmark 는 기본적으로 trailing 에 위치한 애잖아여??

커스텀 액세서리 위치를 leading 으로 하겠다고 지정해놓고,,, trailing 에 있는 checkmark 뒤에 놓겠다..?? 라는 모순적인 상황이 되는거져

이러한 상황의 결과는, 액세서리가 셀에 추가되지 않았을때의 상황과 동일하게 position(after:) 는 custom accessory 를 가장 뒤에, position(before:) 는 custom accessory 를 가장 앞에 배치합니다.

position(after: .checkmark()) / position(before: .checkmark())

UICellAccessory.CustomViewConfiguration 의 초기화 함수를 보다가 여기까지 왔는데요!

init(customView: UIView, 
placement: UICellAccessory.Placement,
isHidden: Bool?,
reservedLayoutWidth: UICellAccessory.LayoutDimension?,
tintColor: UIColor?,
maintainsFixedSize: Bool?)

마지막으로 maintainsFixedSize 만 보면 되겠네요!!

얘는 custom view 의 frame size 를 유지할건지 결정한다고 해요. 기본값은 false 입니다.

이 값이 true 로 설정되어있으면 system 은 view의 현재 frame size 를 유지하고, false 면 system 은 액세서리를 레이아웃하는 동안 view 의 크기를 조정합니다.

When the value of this property is true, the system preserves the current frame size of the view. When the value of this property is false, the system sizes the view during layout of the accessories.

근데 사실 무슨 말인지도 잘 이해가 안가고,, 제가 truefalse 로 확인해봤을때는 그냥 똑같아 보이는데.. 어떤 조건을 다르게 해야 좀 차이를 볼 수 있는지 모르겠슴다! 아시는 분 있으면 알려주세여!! (뻔뻔)

마무리

이렇게해서 list cell 에 설정할 수 있는 많은 액세서리들에 대해 하나씩 알아봤는데여 뭐 대충 이런게 있구나~~ 하고 알아가면 될 것 같슴다

그래도 글 쓰는데 시간 꽤나 들인거 같은데,, 읽는데는 너무 후루룩이네여..? 족굼 억울하지만 설날이니까 기분 좋게 넘어가는걸로~~~

그럼 모두 모두 해피 설날 보내세요~~!!!

참고

--

--

Responses (1)