1. LayoutDriven UI에 들어가기 앞서..
우리는 iOS에서 UI를 그리거나 업데이트할 때 아래와 같은 이슈를 많이 겪으셨을 것 같아요.
아래의 문제가 모두 UI의 문제라고 볼 수는 없지만, 분명 로직에 따라 UI 업데이트를 요청했는데 반영되지 않는 것이 가장 큰 이슈 같습니다.
- 🆕 뱃지가 있는 UI를 눌렀는데 뱃지가 사라지지 않음
- 📩 메세지를 읽었는데 '읽음' 으로 상태가 바뀌지 않음
- ✅ 버튼을 눌렀는데 Pressed 상태로 바뀌지 않음
- ☠️ UI를 업데이트 하다가 메인쓰레드에서 동작하지 않아 앱이 죽음
이런 상황의 공통점은 로직에 따라 UI 업데이트가 되지 않음으로 볼 수 있습니다.
이러한 문제점은 대부분 UI를 업데이트하는 코드가 바깥에 따로 존재하기 때문이라고 생각합니다.
UI를 작은 컴포넌트 단위로 쪼개고, 그 컴포넌트가 본인의 상태가 어떻게 변경될 지 알고 있다면 어떨까요?
그리고 굳이 '너 지금 상태 바꼈어!' 라고 개발자가 하나하나 세팅하지 않고 UI 컴포넌트가 본인의 상태를 바로 캐치할 수 있다면 어떨까요?
위와 같은 특성을 가진게 LayoutDriven UI라고 생각합니다.기본적으로 로직과 완전히 분리된 컴포넌트로 제작하게 되면 더욱 금상첨화일 것 같네요.
2. Button에게 청사진을 그려주자
위 이미지처럼 버튼 컴포넌트는 총 3가지의 상태를 가지는 요구사항을 구현하는 상황이라고 가정해봅시다.
더불어 이 3가지 상태를 구분하는 Swift에서의 변수를 상상해서 적어 볼게요.
- 활성화 상태
- userinteractionenabled가 true일 경우
- highlighted 값이 false일 경우 - 눌린 상태
- userinteractionenabled가 true일 경우
- highlighted 값이 true일 경우 - 비활성화 상태
- userinteractionenabled가 false일 경우
이미 눌린 상태, 비활성화를 체크하는 변수는 제공해주고 있으니까 새로 만들지 않고도 가능합니다.
iOS에서 기본적으로 제공해주는 변수를 overriding 해서 우리가 원하는 정보로 바꿀 수 있을 것처럼 보이네요.
해당 변수를 적극 활용해서 구현 해보도록 하겠습니다.
3. 코드로 작성 해보자.
LayoutDriven UI는 코드로 작성하면 더욱 머리 속에 그림이 잘 그려지는데요.
아래의 코드를 확인하면서 LayoutDriven UI가 어떻게 구성이 되는지 상상하면서 확인 하시면 더욱 도움 될 것 같습니다.
3-1. 버튼 상태 3가지
가장 먼저 버튼 상태 3가지를 나타낼 enum을 작성합니다.
여기까지는 LayoutDriven UI뿐만 아니라 다양한 컴포넌트를 작성할 때 많이 사용하고 계실 것 같습니다.
public enum BoxButtonStatus {
case active /// 활성화 상태
case inactive /// 비활성화 상태
case pressed /// 눌린 상태
}
public class BoxButton: UIButton {
public var btnStatus: BoxButtonStatus = .inactive {
didSet { setNeedsLayout() }
}
public init(frame: CGRect, btnStatus: BoxButtonStatus) {
self.btnStatus = btnStatus
super.init(frame: frame)
}
}
기본적으로 btnStatus는 비활성화 상태로 세팅합니다.
활성화 또는 눌린 상태는 최초 init 함수에서 바깥에서 주입할 수 있도록 하면 됩니다.
3-2. highlighted / userinteractionenabled 체크
실제로 버튼이 눌렸는지 UI 컴포넌트를 사용하는 곳에서 userinteractionenabled를 false로 바뀐 것을 어떻게 감지할 수 있을까요?
아래 코드에서 setNeedsLayout에 집중해주세요
public class BoxButton: UIButton {
public var btnStatus: BoxButtonStatus = .inactive {
didSet { setNeedsLayout() }
}
public override var isHighlighted: Bool {
didSet { setNeedsLayout() }
}
public override var isUserInteractionEnabled: Bool {
didSet { setNeedsLayout() }
}
public override func layoutSubviews() {
super.layoutSubviews()
// 비동기적으로 layoutSubviews가 호출됨.
}
}
btnStatus와 isHighlighted, isUserInteractionEnabled 변수가 세팅될 때마다 setNeedsLayout()이 호출됩니다.
setNeedsLayout은 비동기적으로 layoutSubviews()를 호출하게 되는데요.
즉 해당 변수가 세팅될 때마다, UI를 업데이트하게 됩니다.
각 상태에 맞게 그림이 그려질 수 있도록 layoutSubviews() 함수에서 처리해주면,
UI 컴포넌트는 자신의 상태가 바뀔 때마다 상태 변화를 감지하고 변화하게 됩니다.
3-3. layoutSubviews()
코드 길이가 길어져 isUserInteractionEnabled에 대한 처리는 생략하고 나타냈습니다.
아래처럼 layoutSubviews가 호출되고 본인의 상태 값에 따라서 비동기적으로 UI가 다시 그려지는 것을 확인할 수 있습니다.
public override func layoutSubviews() {
super.layoutSubviews()
// 버튼의 상태마다 컬러나 디자인 등을 변화 시킴
switch btnStatus {
case .active:
btnLabel.textColor = Colors.grey.g800
backgroundColor = Colors.tint.sub.n400
case .inactive:
backgroundColor = Colors.grey.g700
btnLabel.textColor = Colors.grey.g500
case .pressed:
btnLabel.textColor = Colors.grey.g800
backgroundColor = Colors.tint.sub.n600
}
// isHighlighted가 되었을 경우 컬러 변경
switch isHighlighted {
case true:
backgroundColor = Colors.tint.sub.n600
case false:
backgroundColor = Colors.tint.sub.n400
}
}
3-4. 성능 개선
"어 이거.. 변수 하나만 바꼈는데 모든 UI를 다 업데이트하는데 성능에 영향이 있지 않을까?"
저도 처음 LayoutDriven UI를 구성할 때 위와 같은 고민을 한 적이 있습니다.
대부분의 UI 컴포넌트는 매우 작은 단위로 구성되기 때문에,
작은 컴포넌트의 UI 업데이트가 성능에 영향을 주는 부분보다 얻는 득이 더 많아 무시할 수 있는 수준이라고 생각했습니다.
다만, 버튼/뱃지와 같은 작은 컴포넌트가 아닌 많은 정보를 담고 변경해야 하는 복잡한 컴포넌트같은 경우에는
분명 성능상 문제가 있을 것처럼 보입니다.
만약 약 50개의 UI 변수가 존재하는데, 1개의 변수만 변경되었는데 변경되지 않는 49개의 UI도 다시 그리게 되니까요.
이전 상태와 현재 상태를 비교해서 변함이 없다면 유지하는 예외 코드를 넣거나,
UI 업데이트가 필요한 변수에 setNeedsLayout() 을 모두 넣지 않고,
각 개별 UI 업데이트가 필요한 별도의 layoutSubviews() 역할을 하는 함수를 만들고
메인 쓰레드에서 동작하게 하면 비슷하게 흉내낼 수 있습니다.
컴포넌트의 역할이 비대해져 하나에서 모두 해결할 수 없다거나
협업을 위한 코드 가독성 개선 방법 중 하나로 위처럼 해결할 수 있습니다.
4. 간단하지만 강력한 친구
현업에서 UI 작업을 하게 될 때 가장 많이 고민하는 것은 UI를 메인 쓰레드에서 효율적으로 호출하게 하는 방법입니다.
API 로직은 최대한 백그라운드에서 실행될 수 있도록 하고,
최종적으로 UI반영만 필요할 때 메인 쓰레드로 변경하고자 하는 것인데요.
최초에는 올바르게 구현했을지라도, 이후에 다른 개발자가 API나 UI 로직을 수정하게 되면서
백그라운드 쓰레드로 실행된 API가 UI를 직접 세팅하게 되면 앱이 죽게 됩니다 ☠️
이러한 상황을 미연에 방지하고자, LayoutDriven UI를 활용한다면
서버 응답에 따라 비동기적으로 UI를 세팅해야 할 경우 개발자의 실수로 백그라운드 쓰레드에서 동작되어
앱이 Crash 되거나 UI 업데이트가 지연되는 현상을 방지할 수 있습니다.
'iOS > UI' 카테고리의 다른 글
[iOS] SnapKit 사용해보기 (0) | 2021.05.08 |
---|