일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 부트캠프
- Failed to register bundle identifier
- roundingMode
- viewcontroller
- xcode
- 독후감
- Codegen
- 스타트업주니어로살아남기
- Mock
- UIResponder
- 책후기
- delegation
- Structures and Classes
- NumberFormatter
- IOS
- NotificationCenter
- SWIFT
- Info.plist
- 야곰아카데미
- View Life Cycle
- 아이폰
- 스위프트
- @available
- 독서후기
- contentInset
- SWIFTUI
- Navigation
- human interface guidelines
- Modality
- mvvm
- Today
- Total
호댕의 iOS 개발
[iOS] 가로 / 세로 모드 처리하기 (viewWillTransition) + FlowLayout에서 center paging이 되는 셀 구현하기 + safeArea의 padding 값 구하기 본문
[iOS] 가로 / 세로 모드 처리하기 (viewWillTransition) + FlowLayout에서 center paging이 되는 셀 구현하기 + safeArea의 padding 값 구하기
호르댕댕댕 2022. 12. 4. 17:18
이전에 Center Paging을 구현하기 위해 Compositional Layout을 사용했다.
이를 사용하면 쉽게 Center Paging을 구현할 수 있었는데, 이를 사용하다가 iOS 15 미만에서 계속 강제종료되는 문제가 있어서 Flow Layout을 사용하면서 center paging을 구현하게 됐다.
(당시 강제 종료되는 문제는 자동으로 셀이 넘어가는 배너에 Compositional Layout을 사용했었는데 iOS 15 미만 일정 사용자에게서 발생했다 -> 일부 사용자에게서만 발생한 문제라 정확한 원인은 파악하지 못했다)
Flow Layout에서 Center Paging 구현하기
final class CenterPagingCollectionViewFlowLayout: UICollectionViewFlowLayout {
private var previousOffset: CGFloat = 0
private(set) var currentPage: Int = 0
init(currentPage: Int = 0) {
self.currentPage = currentPage
super.init()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func targetContentOffset(
forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint
) -> CGPoint {
guard let collectionView = collectionView else {
return super.targetContentOffset(
forProposedContentOffset: proposedContentOffset,
withScrollingVelocity: velocity
)
}
let itemsCount = collectionView.numberOfItems(inSection: 0)
if previousOffset > collectionView.contentOffset.x && velocity.x < 0 {
currentPage = max(currentPage - 1, 0)
} else if previousOffset < collectionView.contentOffset.x && velocity.x > 0 {
currentPage = min(currentPage + 1, itemsCount - 1)
}
let updatedOffset = (itemSize.width + minimumInteritemSpacing) * CGFloat(currentPage)
previousOffset = updatedOffset
return CGPoint(x: updatedOffset, y: proposedContentOffset.y)
}
}
해당 클래스를 채택하면 Center Paging이 가능한 CollectionView를 구현하도록 UICollectionViewFlowLayout을 상속받는 클래스를 만들었다.
이때 targetContentOffset 메서드를 재정의해서 사용했다.
Retrieves the point at which to stop scrolling.
스크롤이 멈추는 point를 찾아주는 메서드이다.
content offset은 좌표로 스크롤을 하거나 스와이프를 하게 되면 content offset이 변하게 된다.
만약 스크롤이 됐을 때 위치를 지정하고 싶다면 이 메서드를 재정의하여 스크롤이 중지될 지점을 지정할 수 있게 된다.
여기서 item의 너비와 아이템 간 거리를 더한 다음 현재 페이지를 velocity를 통해 구해 이를 곱해줘서, 아이템 간 거리가 존재하더라도 스크롤을 할 때 이 거리만큼 밀리지 않고 스크롤이 될 수 있도록 구현해주었다.
하지만 여기서 가로모드로 변경하게 되면 문제가 발생한다. 세로 모드 기준으로 item의 너비가 적용되어 스크롤이 끝까지 되지 않는 문제가 생기는 것이다.
당연히 가로모드로 되면 아이템의 너비가 늘어나게 되는데 이에 대응하지 못해 발생한 문제이다.
그리고 종종 아이템의 중앙이 아닌 애매한 위치로 넘어간 상태로 가로모드 / 세로모드 전환이 되기도 한다.
가로모드 / 세로모드 전환 대응
SafeAreaInsets 구하기
일단 가로모드 / 세로모드 대응을 하기 위해선 일반적인 경우 leading / trailing의 레이아웃을 safeArea와 잡아야 한다.
아이폰 X부터 노치가 생겼기 때문에 safeArea와 잡지 않는다면 노치에 글씨가 가리는 경우가 대부분이기 때문이다. 세로모드일 때에는 safeArea가 디스플레이 좌우에 딱 붙어있고, 따로 padding이 없기 때문에 신경쓸 부분이 많이 없었지만 가로모드의 경우 이야기가 달라진다.
가로모드가 되는 경우 노치가 좌 / 우 중 하나가 되기 때문에 safeArea의 padding 값이 적용되기 때문이다.
그래서 우리는 safeArea의 padding 값을 먼저 구해줘야 한다.
private func getHorizontalSafeAreaInsets() -> (right: CGFloat, left: CGFloat) {
var rightPadding: CGFloat
var leftPadding: CGFloat
if #available(iOS 15.0, *) {
let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
rightPadding = scene?.windows.first?.safeAreaInsets.right ?? 0
leftPadding = scene?.windows.first?.safeAreaInsets.left ?? 0
return (rightPadding, leftPadding)
}
rightPadding = UIApplication.shared.windows.first?.safeAreaInsets.right ?? 0
leftPadding = UIApplication.shared.windows.first?.safeAreaInsets.left ?? 0
return (rightPadding, leftPadding)
}
iOS 15 이전에는 UIApplication.shared.windows의 첫 번째 window에 접근해 safeAreaInsets를 구할 수 있었지만, iOS 15.0 이후부터는 UIApplication.shared.connectedScenes를 통해 UIWindowScene의 배열을 구한 후 여기서 windows에 접근해 safeAreaInsets를 구해야 한다.
좀 더 간단한 방법이 있을 수 있지만 내가 찾은 방법은 이거였다.
UIWindowScene은 앱의 하나 이상의 window를 관리하는 Scene으로, iOS 13부터 Multiple window를 지원하게 되면서 생성된 개념이다.
이는 이전의 멀티 태스킹과는 차이가 있다. 멀티 태스킹은 다른 앱을 동시에 띄우는 것이지만 Multiple window는 하나의 앱을 두 개의 scene로 띄우는 것으로 알고 있다.
공식문서에선 UIWindowScene 인스턴스를 직접 만들지 말라고 하고 있다.
어찌됐든 이를 통해 safeAreaInsets를 구하게 된다.
이때 safeAreaInsets는 leading과 trailing이 아닌 right와 left로 padding값을 구하게 되는데 leading / trailing이 문화권에 따라 글 읽는 방향이 다르기 때문에 이에 대응하기 위해 나온 개념이라면 safeAreaInsets의 경우 정말 기기의 좌우와 관련된 개념이라 leading / trailing이 없다고 판단했다.
(이건 정말 뇌피셜이라 잘못된 정보일 수 있습니다)
이렇게 했다면 가로모드 / 세로모드 전환이 되는 것을 인지할 수 있는 메서드가 있어야 한다.
가로모드 / 세로모드 전환 인식하기
이런 역할을 하는 함수가 바로 viewWillTransition(to:with:)이다.
이전에 분명 사용한 적이 있었지만 사용을 안하다보니 기억 속에서 잊혀졌었다...
이 함수는 view의 크기가 변경되었다는 것을 알려준다.
UIKit이 ViewController의 view 크기를 변경하기 전에 이 메서드를 호출하게 된다.
따라서 이 메서드를 재정의해서 크기 변경과 관련된 추가 작업을 수행할 수 있는 것이다.
하지만 문제가 있었다.
가로모드 / 세로모드 전환으로 인해 view의 크기가 변경되는 것은 잘 알려주고 있었지만...
이름에 맞게 변경되기 직전에 호출되어 view의 크기가 변경된다는 것을 알려주는 메서드라 safeAreaInsets가 회전되기 전을 기준으로 나오는 것이다.
에이 그럼 당연히 viewDidTransition이 있겠지 뭐~~ 여기서 처리하자
라고 생각했지만...
역시 당연한 건 없다... 이런 메서드는 존재하지 않았다.
하지만 역시 방법은 항상 있었다.
viewWillTransition에는 coordinator라는 파라미터를 받게 된다.
공식문서에 보면 전환에 대한 정보를 이 객체를 통해 얻거나 변경사항을 animate할 수 있다고 한다.
흠... 아직 명확하게 이해는 가지 않는다.
UIViewControllerTransitionCoordinator의 공식문서를 살펴보자
A set of methods that provides support for animations associated with a view controller transition.
ViewController의 전환과 관련된 애니메이션을 지원하는 메서드들의 묶음이라고 한다.
일반적으로 우리는 이 프로토콜을 채택하지 않는데, UIKit에서 ViewController를 present하거나 dismiss할 때 이 전환 coordinator 객체를 자동으로 생성해 ViewController의 프로퍼티에 할당하고 있다고 한다.
이는 애니메이션이 지속되는 동안에는 일시적으로 지속된다.
이 프로토콜에선 animate(alongsideTransition:)이라는 함수를 가지고 있다.
전환 애니메이션과 동시에 지정된 애니메이션을 실행하는 함수인 것이다.
여기서 SafeAreaInsets를 구하게 되면 전환이 되면서 클로저에 정의한 애니메이션이 동작하게 됨으로 변경된 SafeAreaInsets를 구할 수 있게 된다.
viewDidTransition은 없지만 이와 유사한 동작을 구현할 수 있는 것이다.
그래서 여기서 collectionView의 itemSize를 변경해주면!
이전에 UICollectionViewFlowLayout의 targetContentOffset 메서드에서도 현재 itemSize에 맞게 스크롤이 가능하도록 해준다.
override func viewWillTransition(
to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator
) {
guard
let collectionViewLayout = qnaCollectionView.collectionViewLayout
as? CenterPagingCollectionViewFlowLayout
else {
return
}
coordinator.animate { [weak self] _ in
guard let self = self else { return }
let rightPadding: CGFloat = self.getHorizontalSafeAreaInsets().right
let leftPadding: CGFloat = self.getHorizontalSafeAreaInsets().left
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.orientation.isLandscape {
collectionViewLayout.itemSize = CGSize(
width: size.width - rightPadding - leftPadding,
height: 150
)
let currentOffset: CGPoint = CGPoint(
x: CGFloat(collectionViewLayout.currentPage) * (size.width - rightPadding - leftPadding),
y: .zero
)
self.collectionView.setContentOffset(
currentOffset,
animated: true
)
} else {
collectionViewLayout.itemSize = CGSize(
width: size.width,
height: 150
)
let currentOffset: CGPoint = CGPoint(
x: CGFloat(collectionViewLayout.currentPage) * size.width,
y: .zero
)
self.collectionView.setContentOffset(
currentOffset,
animated: false
)
}
collectionViewLayout.prepare()
collectionViewLayout.invalidateLayout()
}
}
참고자료
- https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617729-targetcontentoffset
- https://developer.apple.com/documentation/uikit/uiwindowscene
- https://developer.apple.com/documentation/uikit/uicontentcontainer/1621466-viewwilltransition
- https://developer.apple.com/documentation/uikit/uiviewcontrollertransitioncoordinator
'Software Engineering > iOS' 카테고리의 다른 글
[iOS] 제너럴하게 사용할 수 있는 Custom PickerView를 구현해보자 (2) | 2023.01.01 |
---|---|
[배포/인증서] 인증서를 갱신해보자 (Distribution / APNs) (0) | 2022.12.12 |
[배포] Appstore에 프로젝트를 배포해보자. (0) | 2022.12.02 |
[RxSwift] 버튼을 누르는 이벤트를 일정 시간 동안 한 번만 보내고 싶다면 (Throttle, Debounce) (0) | 2022.11.07 |
[iOS] AB 테스트를 위한 툴, Hackle (0) | 2022.11.07 |