코드 개선 - 전개편 (2)

코드 개선 - 전개편 (2)

UIScreen.main is deprecated..? 코드 방제 전문가가 되어보자.

·

5 min read

요약: 시작은 UIScreen.main의 사용 불가였는데 SwiftUI를 적극 활용해 화면 크기별 반응형 UI 만들게 된 내용이다.


이젠 notificationdelegate를 정리해야 했다. 이 부분은 크게 건드릴 게 없었다. 찾아보니 델리게이트는 끽해봐야 TableView와 CollectionView에 대한 것이었다. 노티피케이션은 Notification.Name에 extension을 만들어 사용할 이름들을 필요할 때마다 쉽게 사용할 수 있도록 추가해두는 것 정도였다.

이번 편의 본격적인 내용인 iOS16 대응에 관련된 이야기를 해보겠다. 이게 참 어려운 문제였는데, iOS16 대응을 위해 UIScreen.main.bounds.width를 사용할 수 없었다. 그런데 출시가 4월이었으니 실상 이미 iOS 16이 모두에게 배포된 이후였다. (진짜 출시된 게 점점 신기해진다) 늑장 대응에 알고 있음에도 안 고친 내 죄인 것을 알고 있다. (반성 많이 했다)

아무튼 그래서 이번에는 iOS16 대응을 위해 deprecated되었다는 비권장 코드를 제거하는 이야기다. 그런데 이제 SwiftUI가 출연하는데 UIKit도 함께 출연해서 둘이 쌍벽을 이루고 개발자를 감질맛 나게 하는 그런 내용의 4K 다큐멘터리는 아니고 평범하게 결함을 찾아 제거하는 코드 방제 이야기다.


일단 저번 회차의 네비게이션 문제 해결로 UIScreen.main을 사용하는 코드는 반으로 줄었지만, 여전히 많은 상태였다. 그래서 일단 어디에 이 코드가 들어가는지 파악부터 해보았다. 두 군데에 사용하고 있었는데, 탭바의 모양을 담당하는 UI 요소와 collectionView의 셀 크기를 구하기 위해 사용하고 있었다.

나는 손쉽게 그들의 위치를 찾을 수 있었다. 당연하다. 검색하면 나오니까... 셀 크기를 구하는 부분에서는 그냥 사람들이 구하라는대로 self.view.window?.windowScene?.screen.bounds.size를 사용했다. 그런데 이게 참 문제인 게 이 녀석 옵셔널이라 다른 값도 줘야한다... 대체값은 그냥 처음 뷰컨트롤러에서 self.view.bounds.size로 줬다. 일단 이걸 사용하는 코드를 아예 없앨 수는 없는 상황이어서 AppDelegate의 screenSize라는 변수를 하나 만들어 저장했다. 그리고 UIKit에서의 방제는 모두 마쳤다.

그런데 여기까지는 문제가 아니었다.

Season 5 Episode 24 GIF by The Big Bang Theory

나에게 찾아온 가장 큰 문제는 내가 이걸 SwiftUI에서도 사용해야 했다는 것이다.

Season 3 Yes GIF by The Big Bang Theory

UIKit에서 패스로 안하고 SwiftUI의 도형 차집합으로 만든 아름다운 나의 탭바를 위해... 머리 좀 굴려야했다. 그래서 방법을 좀 알아봤는데 SwiftUI에서 이걸 하려면 꽤나... 많이 복잡한 길을 가야할 것 같았다.

그런데 "그냥 UIScreen.main.bounds.width 대신에 다른 값을 넣어주면 되지 않냐"라 묻는다면... SwiftUI에서는 힘든 일이었다. 안해본 것도 아니다. 일단 UIScreen.main.bounds.width의 대체제로 애플에서 제안한 self.view.window?.windowScene?.screen.bounds.size이 ViewController를 통해 접근해야 했었다. 그리고 SwiftUI에서는 View를 지정할 때 .onAppear로만 다뤄야 했기 때문에 ViewDidLoad() 와는 차이가 있고 처음 로드 시 문제를 일으켰다.

고심하던 중 떠오른 생각. 피그마에서 디자인 했던 경험이 떠올랐다. 난 분명 이 때 주요 논점이 된 부분에 수치를 입력하지 않았었다. 그럼 거기서 했던대로 작동하게 코드를 짜면 되잖아..!

근데 어떻게?


이 '어떻게'를 해결하기 위해 다시 탭바 코드를 들여다보았다.

기존 탭바 코드에서는 아래와 같이 값을 지정해둔 상태였다. 그리고 그 값을 필요할 때마다 불러오고 갱신하며 사용했다.

@State var screenWidth: CGFloat = UIScreen.main.bounds.width
var body: some View {
    ZStack (alignment: .bottom){
        //screenWidth 변수 사용 및 UI 코드
    }
}

그래서 이제 변수를 아예 제거하고 오류가 나는 부분들을 살펴보았다. (오류가 나는 부분은 아니고 중요한 부분만 옮겼다)

//코드 상략
ZStatck(alignment: .bottom){
Rectangle()
     .frame(width: screenWidth - 140, height: 80)
//코드 중략
}
.position(x: screenWidth - 69, y: 28)
//코드 하략

오류가 나는 부분이자 screenWidth의 역할은 다음의 두 가지로 분류할 수 있었다.

  • UI 구성요소의 길이를 결정한다.

  • UI 구성요소의 절대적인 위치를 결정한다.

위 두 가지 역할을 수행하고 있었다. screenWidth가 사라졌으므로 이 두 가지를 다른 것에 의존하여 결정할 수 있어야 한다. 그래서 '왜 screenWidth를 사용하여 작성하게 되었는가?'와 '주위의 위치를 인식할만한 다른 구성요소가 정말 없는가?'를 고려하며 관련 UI 구성요소들을 다시 살펴보았다.

그리고 나온 결론은 '관련 UI 구성요소의 길이는 화면 길이와 뷰 길이에 대응되어야 한다. (임의의 값으로 시작하는 것은 장애가 발생한다)' 그리고 '관련 UI 구성요소의 절대적인 위치는 오른쪽 정렬 시 동일하게 유지할 수 있다.'라는 것이었다.


그럼 이 조건들로 어떻게 코드를 작성할 수 있을까? 우선 쉽게 해결할 수 있을 것 같은 길이 결정부터 해결했다. 사실, 이 부분은 꽤 쉽게 해결했는데... 일단 어떻게 생긴 탭바인지 인지하고 코드를 보자.

이게 이렇게만 되어있으면 상관 없지만, Monthly Piece는 iPad와 iPhone 겸용이다. iPad에서는 탭바가 길어졌다가 짧아져야 하고 우측에 + 버튼이 고정되어 있어야 한다.

HStack (alignment: .bottom, spacing: 0){
    Rectangle()
        // .frame(width: screenWidth - 140, height: 80)
        .frame(height: 80)
    Rectangle()
        .frame(width: 140, height: 64)
}

주석 처리한 부분이 원래 사용하던 코드다. 화면을 뚫어져라 보던 중 SwiftUI를 처음 익힐 때 Apple의 공식 문서를 보고 따라한 기억이 떠올랐다. 내 기억을 스쳐 지나간 것이 spacing이다. spacing은 내부 요소들 사이의 여백을 지정하는 요소다. 즉, HStack에서 가로로 나열된 요소들 사이에 공백을 0으로 하겠다는 의미의 코드가 되는 것이다.

그리고 SwiftUI에서는 frame 값에 width나 height를 비울 수 있다. UIKit에서도 widthAnchor가 없더라도 leadingAnchor와 trailingAnchor를 사용해 나타내면 의도한 대로 나타나듯이 말이다. 이 코드를 보면 HStack 안에 두 개의 사각형이 있는데, 높이는 둘 다 80으로 고정이고 너비는 하나가 140으로 고정이며 여백값이 0이다. 전체 너비는 부모 뷰에서 결정하고 있으므로 너비를 표기하지 않은 사각형의 너비는 (부모 뷰의 너비) - (한 사각형의 고정 너비: 140)로 계산할 수 있다.

그러면 부모 뷰를 전체 뷰의 너비와 동일하게 한다면 전체 뷰가 작아지고 커지는 것, 화면 회전 등에 대응하여 값을 입력할 필요 없이, UI 구성요소의 길이를 조절하는 동시에 결정할 수 있다. HStack의 부모 뷰 너비는 이미 전체 뷰의 너비와 같았으므로 이는 쉽게 해결할 수 있었다.

그리고 이제 코드를 조금만 바꿔도 망가지던 부분인 위치 결정을 도전했다. 관련 UI 구성요소는 Circle()을 예시로 들겠다.

ZStack(alignment: .bottomTrailing) {
    Circle()
        .frame(width: 40, height: 40)
        // .position(x: screenWidth, y: 40)
        .offset(x: -118, y: -40)
}

마찬가지로 주석 처리한 부분이 원래 사용하던 코드다. 이 원 같은 경우 크기는 고정이었으며 위치만 화면 크기에 따라 바뀌었다. 우선 UI 요소들을 담고 있던 ZStack의 정렬을 botom에서 bottomTrailing으로 변경했다. 오른쪽 정렬을 통해 얻을 수 있는 이점은 고정 값으로 위치를 정해줄 수 있다는 것이었다. 그러면 전체 길이를 알 필요가 없으므로 screenWidth 변수를 제거할 수 있다.

그리고 position에서 offset으로 변경했다. 이와 관련해서는 offset 과 position 의 layout 단계(naljin, Medium)을 참고하면 큰 도움이 될 것이다. 내가 변경한 이유는 이 뷰의 가용 크기 때문에 UI가 위치를 잘못 잡는 일이 생기고 서로 어긋나 보였기 때문이다.


이렇게 문제를 해결했다. 쓰고나니까 꽤 쉬워 보인다. 역시 공부하는 만큼 보이는 것 같다. 이렇게 길이에 집착하는 이유 중 하나는 아이패드에서 분할 화면을 지원하기 때문이었고, 성공적으로 작동했다. 아주 만족스럽다. 아직도 고쳐야할 부분은 산더미지만 개선되고 있다는 데 뿌듯함을 느낀다.