🪽 Sendbird
뷰의 라이프사이클을 잘 활용하고 있는 예시를 찾아보다가 Sendbird 레포지토리를 발견했었다. 이번에 앱의 라이프사이클을 공부하고 뷰의 라이프사이클을 공부하면서 Sendbird 레포지토리를 다시 한번 보고 정리해보고 싶었다.
GitHub - sendbird/sendbird-uikit-ios: Sendbird UIKit for iOS is a development kit with a user interface, offering a simplified i
Sendbird UIKit for iOS is a development kit with a user interface, offering a simplified integration into chat. - sendbird/sendbird-uikit-ios
github.com
물론 이곳이 100% 정답이라고 생각하지는 않지만 뷰의 라이프 사이클을 잘 활용하고 있는 좋은 예시라고 생각한다.
🕹️ SBUBaseViewController
2달전에도 업데이트된 것 보니 꾸준히 업데이트되고 있는 것 같다. 한번 살펴보자!

우선 setupViews, setupLayouts를 loadView에서 하고, setupStyles를 viewDidLayoutSubviews에서 한다. 또한 Layouts과 Styles를 업데이트하는 메서드도 있다. 센드버드는 viewDidLoad에서 어떠한 것도 하지 않았다는 것도 신기했고, UIViewController의 메서드 중에서 viewDidLayoutSubviews에서 스타일을 세팅해줬다는 것도 새롭게 다가왔다.
open class SBUBaseViewController: UIViewController, UINavigationControllerDelegate {
// MARK: - Lifecycle
open override func loadView() {
super.loadView()
self.setupViews()
self.setupLayouts()
}
open override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.setNeedsStatusBarAppearanceUpdate()
self.navigationController?.delegate = self
}
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.setupStyles()
}
// MARK: - Sendbird UIKit Life cycle
open func setupViews() { }
open func setupLayouts() { }
open func setupStyles() { }
open func updateLayouts() { }
open func updateStyles() { }
open func updateStyles(needsToLayout: Bool) { }
}
🦶 ViewController 에서의 호출 순서
코드베이스로 UIKit을 구성하게 된다면 호출 순서는 아래와 같다. 뷰가 메모리에 로드되고, 나타나기 전에 레이아웃을 잡는다.
init(nibName:bundle:) / nibName: nil / bundle: nil
loadView()
viewDidLoad()
viewWillAppear(_:)
viewWillLayoutSubviews()
viewDidLayoutSubviews()
viewDidAppear(_:)
참고로 스토리보드로 UIKit을 구성하게 된다면 아래와 같이 찍히게 된다.
init(coder:) / coder: <UINibDecoder: 0x105017a00>
loadView()
viewDidLoad()
viewWillAppear(_:)
viewWillLayoutSubviews()
viewDidLayoutSubviews()
viewDidAppear(_:)
🤨 viewDidLoad 가 아닌 loadView 인 이유?
우선 setupViews와 setupLayout는 우선 뷰의 레이아웃이 잡히기 전에 설정해 주면 가장 좋을 것이라고 와닿는다. 그렇다면 Sendbird는 어째서 viewDidLoad가 아닌 loadView에서 setupViews, setupLayouts를 해준 것일까? 조금더 생각해 보니 loadView가 더 타당하다고 생각이 들었다. 왜냐하면 view를 메모리에 올리면서 같이 다른 view들도 메모리에 올리고 설정해줘도 문제없었다. 만약에 문제가 없다면 최대한 빠르게 이러한 setup작업들을 해주는 것이 좋기 때문에 loadView에서 하는게 좋다는 생각이 들었다.
🤨 viewDidLayoutSubviews 에서 setupStyles 를 해주는 이유?
setupStyle은 레이아웃이 잡히고 해 준다는 의도인 것 같지만 여기서 궁금했던 것은 Style 중에서 레이아웃에 영향이 있는 것들이 있지 않나? 였다. 이부분은 코드를 좀 살펴봤다.
뷰의 프레임이 결정된 후에 적용해야 하는 cornerRadius나 gradient 같은 경우는 이해가 가지만 아래와 같이 font를 변경하거나 numberOfLines = 0 등 layout에 영향을 줄 것만 같은 속성들이 실제 레포지토리에서도 설정되고 있었다.
public override func setupStyles() {
super.setupStyles()
self.titleLabel.font = theme.userMessageFont
self.titleLabel.numberOfLines = 0
self.titleLabel.textColor = theme.userMessageLeftTextColor
self.titleLabel.backgroundColor = .clear
self.container.layer.cornerRadius = 16
self.container.backgroundColor = theme.leftBackgroundColor
}
확실히 numberOfLine의 경우는 setupLayout쪽에서 해줘도 문제가 없을 것 같다. 하지만 이것또한 Layout은 보통 오토레이아웃을 잡을때 많이 활용되기 때문에 가독성을 위해서 setupStyles도 맞아보인다. 또한 폰트의 경우 스타일에 포함되고, theme까지 있어서 업데이트를 위해서는 viewDidLayouSubviews, viewWillLayoutSubviews쪽이 좋아보인다. 스타일 적용은 Layout이 잡히고 적용하는 경우도 많기 때문에 viewWillLayoutSubviews를 택한 것 같다.
🪟 SBUView
setupViews, setupLayouts 등 ViewController와 매우 비슷하다. setupActions이 있다는점이 다르다. 또한
@available(*, unavailable, renamed: "init(frame:)")를 통해 코드베이스로만 View를 만드는 것을 알 수 있다.
open class SBUView: UIView {
public init() {
super.init(frame: .zero)
self.setupViews()
self.setupLayouts()
self.setupActions()
}
public override init(frame: CGRect) {
super.init(frame: frame)
self.setupViews()
self.setupLayouts()
self.setupActions()
}
@available(*, unavailable, renamed: "init(frame:)")
public required init?(coder: NSCoder) {
super.init(coder: coder)
self.setupViews()
self.setupLayouts()
self.setupActions()
}
open override func layoutSubviews() {
super.layoutSubviews()
self.setupStyles()
}
}
extension SBUView: SBUViewLifeCycle {
open func setupViews() { }
open func setupLayouts() { }
open func setupStyles() { }
open func setupActions() { }
open func updateLayouts() { }
open func updateStyles() { }
}
간단하게 초기화때 setupViews, setupLayouts, setupActions를 해준 후 레이아웃이 적용 된 후 setupStyles를 해준다.
🦶UIView 에서의 호출 순서
사실 아래에서 내가 찍어본 호출 순서는 위의 SBUView와는 큰 관련이 없지만... 아래와 같은 순서로 작동한다. 사실 UIView에서 더 살펴봐야 할 것은 setNeedsLayout, layoutIfNeeded의 차이이다.
init(frame:)
willMove(toSuperview:)
didMoveToSuperview()
willMove(toWindow:)
didMoveToWindow()
layoutSubviews()
draw(_:)
🧐 setupActions는 어떻게 작동하고 있을까?
실제로 코드를 살펴보니 Delegate 패턴을 주로 활용하는 듯 보였다. 하지만 재밌는 주석도 발견했다. 😊
// MARK: - Actions
/// `SBUQuotedMessageViewDelegate`
/// - Since: 2.2.0
public weak var delegate: SBUQuotedMessageViewDelegate?
// 액션들에 대한 통일성이 필요
// IMO: 가장 이상적인 케이스는 기본 액션은 우리가 정의해주고 전부 커스터마이징 할 수 잇게 오픈
// 메세지셀 말고 다른 쪽은 핸들러를 제공하고 있지 않고 있음.
var tapHandlerToContent: (() -> Void)?
open override func setupActions() {
self.mainContainerView.addGestureRecognizer(self.contentTapRecognizer)
self.tapHandlerToContent = { [weak self] in
guard let self = self else { return }
self.delegate?.didTapQuotedMessageView(self)
}
}
@objc
open func didTapQuotedMessageView(sender: UITapGestureRecognizer) {
self.tapHandlerToContent?()
}
♻️ setNeedsLayout, layoutIfNeeded
UIView에서 레이아웃이나 스타일이 적용되지 않는 문제를 겪었다면 위의 두 메서드를 무조건 접했을 것이라고 생각한다. 두개의 차이점이 무엇일까?
재밌는 실험을 해보자. 아래와 같이 실행보면 과연 어떻게 나올까? layoutSubviews에서 setNeedsLayout을 한다면?
override func layoutSubviews() {
super.layoutSubviews()
print(#function)
setNeedsLayout()
}
override func setNeedsLayout() {
super.setNeedsLayout()
print(#function)
}
결과는 아래와 같다. layoutSubviews, setNeedsLayout 두개가 무한히 반복된다.

layoutSubviews()
setNeedsLayout()
layoutSubviews()
setNeedsLayout()
layoutSubviews()
setNeedsLayout()
layoutSubviews()
setNeedsLayout()
layoutSubviews()
setNeedsLayout()
... 무한반복!
그렇다면 아래와 같이 layoutSubviews에서 layoutIfNeeded을 한다면?
override func layoutSubviews() {
super.layoutSubviews()
print(#function)
layoutIfNeeded()
}
override func layoutIfNeeded() {
super.layoutIfNeeded()
print(#function)
}
아래와 같이 찍힌다. 심지어 layoutIfNeeded를 호출했는데 layoutSubviews한번쯤 더 해줄만도(?) 한데 해주질 않는다.

layoutSubviews()
layoutIfNeeded()
그 이유는 setNeedsLayout의 경우 다음 RunLoop에서 layoutSubviews를 다시 호출하라고 예약하는 메서드다. 그렇기 때문에 내부에서 호출되게 된다면 무한루프가 발생하는 것이다. layoutIfNeeded의 경우는 말 그대로 필요할 때 즉시 실행(할 게 있을 때 즉시 실행)하는 메서드다. 하지만 변경한 것이 없기 때문에 다시 layoutSubviews를 하지 않는 것이다.
↩️ 뷰를 업데이트 시킨다면?
아래와 같이 frame을 변경해주니 layoutSubviews이 알아서 다시 호출되었다! frame을 다시 설정해주면 자동으로 layoutSubviews가 호출된다! 이때는 setNeedsLayout, layoutIfNeeded는 호출 안해도 된다!
override func viewDidLoad() {
super.viewDidLoad()
let trackingView = LifecycleTrackingView(frame: CGRect(x: 50, y: 100, width: 200, height: 100))
view.addSubview(trackingView)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
trackingView.frame = CGRect(x: 50, y: 100, width: 250, height: 150)
}
}

'→ UIKit' 카테고리의 다른 글
[UIKit] 앱의 라이프 사이클 (0) | 2025.04.07 |
---|
🪽 Sendbird
뷰의 라이프사이클을 잘 활용하고 있는 예시를 찾아보다가 Sendbird 레포지토리를 발견했었다. 이번에 앱의 라이프사이클을 공부하고 뷰의 라이프사이클을 공부하면서 Sendbird 레포지토리를 다시 한번 보고 정리해보고 싶었다.
GitHub - sendbird/sendbird-uikit-ios: Sendbird UIKit for iOS is a development kit with a user interface, offering a simplified i
Sendbird UIKit for iOS is a development kit with a user interface, offering a simplified integration into chat. - sendbird/sendbird-uikit-ios
github.com
물론 이곳이 100% 정답이라고 생각하지는 않지만 뷰의 라이프 사이클을 잘 활용하고 있는 좋은 예시라고 생각한다.
🕹️ SBUBaseViewController
2달전에도 업데이트된 것 보니 꾸준히 업데이트되고 있는 것 같다. 한번 살펴보자!

우선 setupViews, setupLayouts를 loadView에서 하고, setupStyles를 viewDidLayoutSubviews에서 한다. 또한 Layouts과 Styles를 업데이트하는 메서드도 있다. 센드버드는 viewDidLoad에서 어떠한 것도 하지 않았다는 것도 신기했고, UIViewController의 메서드 중에서 viewDidLayoutSubviews에서 스타일을 세팅해줬다는 것도 새롭게 다가왔다.
open class SBUBaseViewController: UIViewController, UINavigationControllerDelegate {
// MARK: - Lifecycle
open override func loadView() {
super.loadView()
self.setupViews()
self.setupLayouts()
}
open override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.setNeedsStatusBarAppearanceUpdate()
self.navigationController?.delegate = self
}
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.setupStyles()
}
// MARK: - Sendbird UIKit Life cycle
open func setupViews() { }
open func setupLayouts() { }
open func setupStyles() { }
open func updateLayouts() { }
open func updateStyles() { }
open func updateStyles(needsToLayout: Bool) { }
}
🦶 ViewController 에서의 호출 순서
코드베이스로 UIKit을 구성하게 된다면 호출 순서는 아래와 같다. 뷰가 메모리에 로드되고, 나타나기 전에 레이아웃을 잡는다.
init(nibName:bundle:) / nibName: nil / bundle: nil
loadView()
viewDidLoad()
viewWillAppear(_:)
viewWillLayoutSubviews()
viewDidLayoutSubviews()
viewDidAppear(_:)
참고로 스토리보드로 UIKit을 구성하게 된다면 아래와 같이 찍히게 된다.
init(coder:) / coder: <UINibDecoder: 0x105017a00>
loadView()
viewDidLoad()
viewWillAppear(_:)
viewWillLayoutSubviews()
viewDidLayoutSubviews()
viewDidAppear(_:)
🤨 viewDidLoad 가 아닌 loadView 인 이유?
우선 setupViews와 setupLayout는 우선 뷰의 레이아웃이 잡히기 전에 설정해 주면 가장 좋을 것이라고 와닿는다. 그렇다면 Sendbird는 어째서 viewDidLoad가 아닌 loadView에서 setupViews, setupLayouts를 해준 것일까? 조금더 생각해 보니 loadView가 더 타당하다고 생각이 들었다. 왜냐하면 view를 메모리에 올리면서 같이 다른 view들도 메모리에 올리고 설정해줘도 문제없었다. 만약에 문제가 없다면 최대한 빠르게 이러한 setup작업들을 해주는 것이 좋기 때문에 loadView에서 하는게 좋다는 생각이 들었다.
🤨 viewDidLayoutSubviews 에서 setupStyles 를 해주는 이유?
setupStyle은 레이아웃이 잡히고 해 준다는 의도인 것 같지만 여기서 궁금했던 것은 Style 중에서 레이아웃에 영향이 있는 것들이 있지 않나? 였다. 이부분은 코드를 좀 살펴봤다.
뷰의 프레임이 결정된 후에 적용해야 하는 cornerRadius나 gradient 같은 경우는 이해가 가지만 아래와 같이 font를 변경하거나 numberOfLines = 0 등 layout에 영향을 줄 것만 같은 속성들이 실제 레포지토리에서도 설정되고 있었다.
public override func setupStyles() {
super.setupStyles()
self.titleLabel.font = theme.userMessageFont
self.titleLabel.numberOfLines = 0
self.titleLabel.textColor = theme.userMessageLeftTextColor
self.titleLabel.backgroundColor = .clear
self.container.layer.cornerRadius = 16
self.container.backgroundColor = theme.leftBackgroundColor
}
확실히 numberOfLine의 경우는 setupLayout쪽에서 해줘도 문제가 없을 것 같다. 하지만 이것또한 Layout은 보통 오토레이아웃을 잡을때 많이 활용되기 때문에 가독성을 위해서 setupStyles도 맞아보인다. 또한 폰트의 경우 스타일에 포함되고, theme까지 있어서 업데이트를 위해서는 viewDidLayouSubviews, viewWillLayoutSubviews쪽이 좋아보인다. 스타일 적용은 Layout이 잡히고 적용하는 경우도 많기 때문에 viewWillLayoutSubviews를 택한 것 같다.
🪟 SBUView
setupViews, setupLayouts 등 ViewController와 매우 비슷하다. setupActions이 있다는점이 다르다. 또한
@available(*, unavailable, renamed: "init(frame:)")를 통해 코드베이스로만 View를 만드는 것을 알 수 있다.
open class SBUView: UIView {
public init() {
super.init(frame: .zero)
self.setupViews()
self.setupLayouts()
self.setupActions()
}
public override init(frame: CGRect) {
super.init(frame: frame)
self.setupViews()
self.setupLayouts()
self.setupActions()
}
@available(*, unavailable, renamed: "init(frame:)")
public required init?(coder: NSCoder) {
super.init(coder: coder)
self.setupViews()
self.setupLayouts()
self.setupActions()
}
open override func layoutSubviews() {
super.layoutSubviews()
self.setupStyles()
}
}
extension SBUView: SBUViewLifeCycle {
open func setupViews() { }
open func setupLayouts() { }
open func setupStyles() { }
open func setupActions() { }
open func updateLayouts() { }
open func updateStyles() { }
}
간단하게 초기화때 setupViews, setupLayouts, setupActions를 해준 후 레이아웃이 적용 된 후 setupStyles를 해준다.
🦶UIView 에서의 호출 순서
사실 아래에서 내가 찍어본 호출 순서는 위의 SBUView와는 큰 관련이 없지만... 아래와 같은 순서로 작동한다. 사실 UIView에서 더 살펴봐야 할 것은 setNeedsLayout, layoutIfNeeded의 차이이다.
init(frame:)
willMove(toSuperview:)
didMoveToSuperview()
willMove(toWindow:)
didMoveToWindow()
layoutSubviews()
draw(_:)
🧐 setupActions는 어떻게 작동하고 있을까?
실제로 코드를 살펴보니 Delegate 패턴을 주로 활용하는 듯 보였다. 하지만 재밌는 주석도 발견했다. 😊
// MARK: - Actions
/// `SBUQuotedMessageViewDelegate`
/// - Since: 2.2.0
public weak var delegate: SBUQuotedMessageViewDelegate?
// 액션들에 대한 통일성이 필요
// IMO: 가장 이상적인 케이스는 기본 액션은 우리가 정의해주고 전부 커스터마이징 할 수 잇게 오픈
// 메세지셀 말고 다른 쪽은 핸들러를 제공하고 있지 않고 있음.
var tapHandlerToContent: (() -> Void)?
open override func setupActions() {
self.mainContainerView.addGestureRecognizer(self.contentTapRecognizer)
self.tapHandlerToContent = { [weak self] in
guard let self = self else { return }
self.delegate?.didTapQuotedMessageView(self)
}
}
@objc
open func didTapQuotedMessageView(sender: UITapGestureRecognizer) {
self.tapHandlerToContent?()
}
♻️ setNeedsLayout, layoutIfNeeded
UIView에서 레이아웃이나 스타일이 적용되지 않는 문제를 겪었다면 위의 두 메서드를 무조건 접했을 것이라고 생각한다. 두개의 차이점이 무엇일까?
재밌는 실험을 해보자. 아래와 같이 실행보면 과연 어떻게 나올까? layoutSubviews에서 setNeedsLayout을 한다면?
override func layoutSubviews() {
super.layoutSubviews()
print(#function)
setNeedsLayout()
}
override func setNeedsLayout() {
super.setNeedsLayout()
print(#function)
}
결과는 아래와 같다. layoutSubviews, setNeedsLayout 두개가 무한히 반복된다.

layoutSubviews()
setNeedsLayout()
layoutSubviews()
setNeedsLayout()
layoutSubviews()
setNeedsLayout()
layoutSubviews()
setNeedsLayout()
layoutSubviews()
setNeedsLayout()
... 무한반복!
그렇다면 아래와 같이 layoutSubviews에서 layoutIfNeeded을 한다면?
override func layoutSubviews() {
super.layoutSubviews()
print(#function)
layoutIfNeeded()
}
override func layoutIfNeeded() {
super.layoutIfNeeded()
print(#function)
}
아래와 같이 찍힌다. 심지어 layoutIfNeeded를 호출했는데 layoutSubviews한번쯤 더 해줄만도(?) 한데 해주질 않는다.

layoutSubviews()
layoutIfNeeded()
그 이유는 setNeedsLayout의 경우 다음 RunLoop에서 layoutSubviews를 다시 호출하라고 예약하는 메서드다. 그렇기 때문에 내부에서 호출되게 된다면 무한루프가 발생하는 것이다. layoutIfNeeded의 경우는 말 그대로 필요할 때 즉시 실행(할 게 있을 때 즉시 실행)하는 메서드다. 하지만 변경한 것이 없기 때문에 다시 layoutSubviews를 하지 않는 것이다.
↩️ 뷰를 업데이트 시킨다면?
아래와 같이 frame을 변경해주니 layoutSubviews이 알아서 다시 호출되었다! frame을 다시 설정해주면 자동으로 layoutSubviews가 호출된다! 이때는 setNeedsLayout, layoutIfNeeded는 호출 안해도 된다!
override func viewDidLoad() {
super.viewDidLoad()
let trackingView = LifecycleTrackingView(frame: CGRect(x: 50, y: 100, width: 200, height: 100))
view.addSubview(trackingView)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
trackingView.frame = CGRect(x: 50, y: 100, width: 250, height: 150)
}
}

'→ UIKit' 카테고리의 다른 글
[UIKit] 앱의 라이프 사이클 (0) | 2025.04.07 |
---|