Roen의 iOS 개발로그

[시행착오]객체 그룹을 만들 때 Array(repeating:count:)를 사용하면 안되는 이유

by Steady On

 

서론

구현하려는 화면

 

코드베이스 UI를 연습하면서 카카오톡 프로필 화면을 구현해보았습니다.

기능 구현은 어차피 안할거고, view를 코드베이스로 짜는 것을 연습하는 것이 목적이었기 때문에 버튼을 굳이 하나하나 만들고, 하나하나 잡기보다 조금 색다른 시도를 해보고 싶었어요. 포인트는 가장 하단에 있는 나와의 채팅, 프로필 편집, 카카오스토리 버튼! 생긴 것도 비슷하고, 위치도 그렇고 뭔가 하나의 그룹으로 묶을 수 있겠다는 생각이 들었습니다. 그래서 각 버튼에 들어갈 buttonTitle과 icon을 열거형으로 정리하고, 마치 IBOutletGroup을 만드는 것처럼 buttonGroup을 만든다음. 레이아웃을 잡는 단계에서 StackView에 차례로 넣는 것으로 구상하고 작업에 들어갔어요.

 

문제 코드

1. 각 버튼의 타이틀과 아이콘을 담고 있는 열거형 만들기

fileprivate enum BottomButtonType: CaseIterable {
    case chatWithMe
    case editProfile
    case kakaoStory
    
    var systemImageName: String {
        switch self {
        case .chatWithMe: return "bubble.left.fill"
        case .editProfile: return "pencil"
        case .kakaoStory: return "clock.badge.fill"
        }
    }
    
    var buttonTitle: String {
        switch self {
        case .chatWithMe: return "나와의 채팅"
        case .editProfile: return "프로필 편집"
        case .kakaoStory: return "카카오스토리"
        }
    }
}

2. Array(repeating:count:) 생성자를 통해서 버튼 그룹을 만들어주고요.

lazy var bottomButtonGroup: [UIButton] = {
    var buttonGroup = Array(repeating: UIButton(), count: BottomButtonType.allCases.count)

    for (button, type) in zip(buttonGroup, BottomButtonType.allCases) {
        var config = UIButton.Configuration.plain()

        var attributedTitle = AttributedString(type.buttonTitle)
        attributedTitle.font = .preferredFont(forTextStyle: .caption1)

        config.attributedTitle = attributedTitle
        config.baseForegroundColor = .white
        config.buttonSize = .small

        config.image = UIImage(systemName: type.systemImageName)
        config.imagePlacement = .top
        config.imagePadding = 15

        button.configuration = config
    }

    return buttonGroup
}()

3. 버튼들이 들어갈 스택 만들기

lazy var buttonStackView: UIStackView = {
    let stackView = UIStackView()
    stackView.axis = .horizontal
    stackView.distribution = .fillEqually
    return stackView
}()
 

4. 스택을 view에 추가해서 레이아웃 잡고, 스택에 버튼을 추가해주기!

buttonStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(buttonStackView)
NSLayoutConstraint.activate([
    buttonStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    buttonStackView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.1),
    buttonStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
    buttonStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 60),
    buttonStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -60)
])

bottomButtonGroup.forEach {
    buttonStackView.addArrangedSubview($0)
}

 

그. 런. 데.....!! (두둥)

문제 화면

 

분명히 버튼을 넣었는데요? 버튼이 없었습니다?!

 

ViewHierarchy

어... 아무튼 없어요;;;;;

 

 

없어요 그냥!!

증발해버린 버튼을 찾아 봅시다.....

 

문제 확인

디버깅의 1단계는 역시 print 찍어보기겠죠? 일단 buttonGroup이 잘 만들어지고 있는지 찍어보았습니다.

BottonButtonGroup 내부에서 bottonGroup을 dump로 찍어보았다

아....? 일단 친구들이 열거형의 제일 마지막 case인 카카오스토리에 대한 정보만 가지고 있네요? 사실 처음에 저는 저것만 보고 열거형이 반복문 내부에서 제대로 안돌아가고 있는건가? 해서 열거형이 잘 돌고 있는지도 print로 찍어봤었어요...

반복문 내부 열거형 print

근데 너무 잘 나와버리는거죠...? 그래서 이상함을 감지하고 dump문을 다시한번 살펴봅니다... 여러분들은 이상한걸 감지하셨나요?

 

문제 원인

빨간 박스가 그려진 부분을 자세히 보면, 배열 내부의 버튼이 3개인데, 3개다 같은 메모리 주소값을 가지고 있다는 것을 알 수 있어요...결국 따지고 보면 0번째 인덱스에도, 1번째, 2번째 인덱스에도 결국 모두 같은 버튼이 들어있다는 말이죠...(가가 가가 가~?) 왜 이같은 현상이 일어났냐!!를 파헤쳐보면, 제가 처음에 버튼 그룹을 만들 때 썼던 Array 생성자에 비밀이 있습니다.

 

https://developer.apple.com/documentation/swift/array/init(repeating:count:)-5y5f0 

 

init(repeating:count:) | Apple Developer Documentation

Creates a new collection containing the specified number of a single, repeated value.

developer.apple.com

공식문서를 살펴보면, 이 생성자는 repeating에 받은 요소를 count만큼 반복해서 새 컬렉션을 만든다고 되어 있는데요.

var buttonGroup = Array(repeating: UIButton(), count: BottomButtonType.allCases.count)

이렇게 코드를 작성하면서 저는 당.연.히!! UIButton의 인스턴스가 생성되면서 서로 다른 객체가 배열에 들어갈 줄 알았는데, 인스턴스의 생성은 단 한번만 일어나고, 그 해당 인스턴스가 반복이 되니까 결국 배열에는 똑같은 버튼이 3번 들어가버리게 되어버린 겁니다...ㅠㅠ

코드로 간단하게 말해보면, 결국 아래와 같이 쓴것과 다름이 없다...는 것이죠....ㅎㅎ

let button = UIButton()
var buttonGroup = Array(repeating: button, count: BottomButtonType.allCases.count)

그럼 이제 문제를 알았으니 해결해봅시다....!

 

해결편

문제의 원인이 배열 생성자로 UIButton의 인스턴스를 만드는 것이기 때문에, 인스턴스를 반복문 안에서 만들어주는 걸로 로직을 변경했어요.

lazy var bottomButtonGroup: [UIButton] = {
    var buttonGroup = [UIButton]()

    for type in BottomButtonType.allCases {
        let button = UIButton()

        var config = UIButton.Configuration.plain()

        var attributedTitle = AttributedString(type.buttonTitle)
        attributedTitle.font = .preferredFont(forTextStyle: .caption1)

        config.attributedTitle = attributedTitle
        config.baseForegroundColor = .white
        config.buttonSize = .small

        config.image = UIImage(systemName: type.systemImageName)
        config.imagePlacement = .top
        config.imagePadding = 15

        button.configuration = config
        buttonGroup.append(button)
    }

    return buttonGroup
}()

처음에 buttonGroup 배열을 빈배열로 초기화하고 반복문을 돌면서 버튼 인스턴스를 만들고, config를 적용해서 배열에 추가하는 방법으로 수정했습니다.

 

이제 버튼 3개가 모두 잘 나오구요!!

당연하게도 Button들이 각각 다른 인스턴스라는 것도 확인할 수 있었습니다 ㅎㅎ

 

마무리

UI를 짜면서도 단순하게 마구마구 컴포넌트를 만들어버리지 않고 어떡하면 재밌게 해 볼 수 있을까 생각하는데요. 덕분에 이런 재밌는 경험(?)을 하게 되어서 만족스러운 실험(?)이었습니다!

블로그의 정보

Roen의 iOS 개발로그

Steady On

활동하기