Swift, Safe Area에 대해서 알아봅니다.

Safe Area in UIView, UICollectionView, UITableView, UIScrollView, UIViewController

Posted by MinJun on Tuesday, September 18, 2018 Tags: Swift   12 minute read

iOS Safe Area의 글을 의역 했습니다.


Contents

  • iOS Safe Area
  • Agenda
  • Example
  • UIView
  • UIViewController
    • Simulate iPhone X safe area
  • UIScrollView
    • Contnet Insets Adjustment Behavior
    • Adjusted Content Insets
  • UITableView
  • UICollectionView
  • Recap

iOS Safe Area

Rosberry의 iOS 개발자 Evgeny M.이 작성했습니다.

iOS 7에서 애플은 어떤 뷰들(상태 바, 네비게이션 바, 탭 바 등) 이 스크린 영역을 가리지 않는 UIViewController에 있는 스크린 프로퍼티인 topLayoutGuide, bottomLayoutGuide를 소개했습니다. iOS 11에서 이 속성들을 금지 시쳤고 safe area를 소개했습니다. Apple은 safe area 밖에 컨트롤을 배치하지 말것을 권고합니다. 이런 이유로 iOS 11에서는 뷰를 배치시킬때 safe area API를 사용해야합니다.

iPhonX의 해상도(resoultion)과 safe area를 지원하기 시작했을때 UIKit의 많은 클래스가 새로운 safe area 기능을 가지고 있음을 알았습니다. 이 글의 목표는 Safe area를 요약하고 설명하는것입니다


Agenda

이 블로그의 글은 다음의 주제로 분리됩니다.

  • UIView
  • UIViewController
  • UIScrollView
  • UITableView
  • UIColelctionView

위의 클레스들은 새로운 safe are properties와 method를 가지는 모든 클래스들 입니다


Example

이 글을 읽는 동안 GitHub repository에 있는 예제를 함께 할수 있습니다.


UIView

iOS11에서 UIViewController의 topLayoutGuide, bottomLayoutGuide는 다음의 새로운 UIView의 safe area 속성으로 변경되었습니다.

@available(iOS 11.0, *)
open var safeAreaInsets: UIEdgeInsets { get }

@available(iOS 11.0, *)
open var safeAreaLayoutGuide: UILayoutGuide { get }

safeAreaInsets 속성은 화면의 상, 하단 뿐만 아니라 모든 면에서 화면을 가릴수 있음을 의미합니다. iPhone X가 새로 나타났을때 왜 왼쪽과 오른쪽 인셋이 필요한지 명확하게 되었습니다.

Landscape orientation 때문에..



iPhone X는 세로 방향(portrait orientation)에서 상, 하 safe area insets를 가집니다. 가로 방향(landscape orientation) 에서는 왼쪽, 오른쪽 safe area inserts를 가집니다.

다음 예제를 봅니다. 텍스트 레이블과 고정된 높이를 가진 두개의 사용자 하위뷰는 뷰 컨트롤러의 뷰에 상단과 하단에 추가되고, 뷰의 가장자리에 위치됩니다.


하위뷰들의 내용은 상단의 노치(notch)와 하단의 홈버튼과 겹치는(overlapped)걸 볼수 있습니다. 하위뷰를 올바르게 배치하려면 수동 레이아웃을 사용하여 safe area에 위치시킬수 있습니다.

topSubview.frame.origin.x = view.safeAreaInsets.left
topSubview.frame.origin.y = view.safeAreaInsets.top
topSubview.frame.size.width = view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right
topSubview.frame.size.height = 300

또는

bottomSubview.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
bottomSubview.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
bottomSubview.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
bottomSubview.heightAnchor.constraint(equalToConstant: 300).isActive = true


더 보기 좋습니다. 뿐만 아니라. 하위 뷰의 하위 클래스에 직접 safe area를 하위뷰 내용에 추가할수 있습니다.

label.frame = safeAreaLayoutGuide.layoutFrame

또는

label.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor).isActive = true
label.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor).isActive = true
label.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor).isActive = true


이제 뷰 컨트롤러 뿐만 아니라 하위 뷰 계층 어느곳에서나 safe area를 뷰에 추가할수 있습니다.


UIViewController

iOS 11에서 UIViewController는 새로운 속성을 가집니다.

@available(iOS 11.0, *)
open var additionalSafeAreaInsets: UIEdgeInsets

뷰 컨트롤러의 하위뷰가 자식 뷰 컨트롤러의 뷰들을 가지고 있을때 사용됩니다. 예를들어 애플은 UINavigationController와 UITabBarController에서 bar가 반투명(translucent)일때 additional safe area insert을 사용합니다.


잘 작동하지만 상태바를 숨겼을때 이상일이 발생합니다.


모든 safe area insert는 알맞게 연산되지만 네비게이션 바는 노치바 아래의 맨위로 이동합니다. 슬픈 버그이며 일부 해결 방법을 제외하고는 현재 해결할 방법이 없다는 것을 알고있습니다.

additional safe area inserts 또는 safe area insert이 시스템에 의해서 변경될때 UIView와 UIViewController의 적절한 메소드가 호출됩니다.

// UIView
@available(iOS 11.0, *)
open func safeAreaInsetsDidChange()

//UIViewController
@available(iOS 11.0, *)
open func viewSafeAreaInsetsDidChange()

Simulate iPhone X safe area

Additional safe area inserts는 앱이 iPhone X를 지원하는 방식을 테스트하는데 사용할 수도 있습니다. 시뮬레이터에서 앱을 테스트할 수 없고, iPhoneX가 없는경우😭 유용합니다.

//portrait orientation, status bar is shown
additionalSafeAreaInsets.top = 24.0
additionalSafeAreaInsets.bottom = 34.0

//portrait orientation, status bar is hidden
additionalSafeAreaInsets.top = 44.0
additionalSafeAreaInsets.bottom = 34.0

//landscape orientation
additionalSafeAreaInsets.left = 44.0
additionalSafeAreaInsets.bottom = 21.0
additionalSafeAreaInsets.right = 44.0



UIScrollView

텍스트 레이블이 있는 스크롤뷰를 뷰컨트롤러에 추가하고 뷰의 가장자리에 위치시킵니다.


보는것과 같이 스크롤뷰 insets는 top 과 bottom에서 자동으로 조정됩니다. iOS 7과 iOS 11이하까지는 UIViewController의 automaticallyAdjustsScrollViewInsets속성을 사용하여 스크롤뷰의 컨텐츠 insets 조정 동작을 관리 했지만 iOS 11에서 더이상 사용하지 않고(deprecated) UIScrollView의 contentInsetAdjustmentBehavior속성으로 대체 되었습니다.

사실 이녀석의 동작 방식 떄문에 safe area에 대해서 찾아보았습니다.

Contnet Insets Adjustment Behavior

never: scroll view content insets는 절대 조정되지 않습니다. 간단합니다


scrollableAxex: 스크롤 가능한 축(scrollable axes)를 위해 컨텐츠 인셋이 조정됩니다. 예를들어, vertical axis는 스크롤뷰 컨텐츠 사이즈가 스크롤뷰 프레임 사이즈 높이 보다 크거나 alwayBoundceVertical 속성이 사용가능할때 스크롤 가능합니다. 비슷하게 horizontal axis는 컨텐츠 넓이가 프레임 넓이보다 크거나 alwaysBounceHorizontal 속성이 사용가능할때 스크롤 가능합니다.


always: 스크롤 가능할때와 그렇지 않을때를 위해 스크롤뷰 content inserts이 조정됩니다.


automaic: 기본값 입니다. 다음 조건이 true일때 always와 동일합니다.

  • 스크롤뷰의 horizontal axis가 스크롤 가능하고, vertical axis는 스크롤 불가능할때
  • scrollview가 뷰 컨트롤러 뷰의 첫번째 하위 뷰일때
  • 뷰 컨트롤러가 네비게이션 또는 탭바 컨트롤러의 자식일때
  • automaticallyAdjustScrollViewInsets이 가능할때

다른 모든 경우 automatic은 scrollableAxes와 동일합니다.

automatic behavior에 대한 추가 설명은 UIScrollView class에서 알수 있습니다.

.scrollableAxes와 비슷하지만 이전 버전과 호환성을 위해 스크롤뷰가 스크롤 가능한지 여부에 관계없이 네비게이션 컨트롤러 내부에서 automaticallyAdjustsScrollViewInsets = YES인 뷰 컨트롤러가 스크롤뷰를 소유할때 위, 아래 contentInset을 조정합니다

애플 문서는 조금 다른 설명을 가집니다.

스크롤뷰가 탭바 컨트롤러 또는 네비 게이션 컨트롤러의 컨텐츠 뷰 일때 콘텐츠는 항상 수직으로 조정됩니다. 스크롤뷰가 수평으로 스크롤 가능한 경우, 수평 컨텐츠 오프셋(offset)은 0이 아닌 safe area insets일때 또한 조정됩니다.

이렇게해서 automatic behavior는 기본(default) 입니다. 왜냐하면 이전 버전과 호환성 때문입니다. iOS 10, 11에서는 수평으로 스크롤가능한 스크롤뷰는 상, 하 같은 insets를 가집니다.

Adjusted Content Insets

iOS 11에서 UIScrollView는 adjustedContentInset라는 새로운 속성을 가집니다.

@available(iOS 11.0, *)
open var adjustedContentInset: UIEdgeInsets { get }

contentInset, adjustedContentInset 이 둘은 무엇이 다른가요? 위에서는 네비게이션 바, 아래에서는 탭 바에 의해서 스크롤뷰가 덮어졌을때 두 값을 출력해 보겠습니다.

adjustedContentInset = contentInset + system inset 더 자세한 내용은 https://developer.apple.com/videos/play/wwdc2018/235/를 참조해주세요.

//iOS 10
//contentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)

//iOS 11
//contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
//adjustedContentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)

이제 모든곳에 contentInset에 10을 추가하고 두 값을 다시 출력하면 다음과 같습니다.

//iOS 10
//contentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)

//iOS 11
//contentInset = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
//adjustedContentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)

iOS 11에서 실제 scroll view content insets은 adjustedContentInset 속성으로 부터 알수(read) 있지만, contentInset 속성에서는 알수 없습니다. 즉, 애플리케이션이 iOS 10과 iOS 11을 모두 지원할때 content insets조정을 위한 다른 로직을 만들어야 합니다.

contentInset을 변경하거나 content insets가 시스템에 의해 조정되면 UIScrollView 및 UIScrollView Delegate의 적절한 메소드가 호출 됩니다.

//UIScrollView
@available(iOS 11.0, *)
open func adjustedContentInsetDidChange()

//UIScrollViewDelegate
@available(iOS 11.0, *)
optional public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView)

UITableView

사용자 정의 해더와 셀을 가진 테이블뷰를 뷰 컨트롤러에 추가하고 뷰의 가장자리에 위치시킵니다.


뷰의 가장자리에 테이블뷰를 위치합니다(insetContentViewsToSafeARea == true). 해더와 셀은 투명(transparent)입니다. 셀의 컨텐츠 뷰는 흰 배경색 입니다 해더의 콘텐츠뷰는 빨강 배경색 입니다.

해더뷰와 셀의 콘텐츠 프레임이 가로방향으로 변경되었음을 알수 있습니다. 동시에 셀과 구분선 프레임은 변경되지 않았습니다. 이것이 새로운 UITableView의 insetsContentViewsToSafeArea 속성으로 관리할수 있는 기본 동작입니다.

@available(iOS 11.0, *)
open var insetsContentViewsToSafeArea: Bool

콘텐츠 뷰 insets의 효과를 없게 하려면 insetsContentViewToSafeArea == false


insetsContentViewToSafeArea == false

이제 header/footer/cell content views 프레임은 각 header/footer/cell 프레임과 같다는걸 알수 있습니다.


UICollectionView

UICollectionView에서 같은 아이템들 목록을 생성하여 시도해봅니다.


CollectionView를 뷰의 가장자리에 위치합니다. 셀은 투명(transparent)하지만 셀의 콘텐츠 뷰는 흰배경색입니다. 해더는 빨강 배경색입니다.

collection view는 UICollectionViewFlowLayout을 사용합니다. 스크롤뷰 방향은 수직 입니다. 해더는 UICollectionReusableView 라는 헤더로 contentView가 없고 자체적인 빨강 배경색입니다.

이미지에서 header/footer/cell 에 inset을 추가하지 않았다는걸 알수 있습니다. 컨텐츠를 올바르게 레이아웃하는 유일한 방법은 header/footer/cell 하위뷰를 header/footer/cell safe area로 붙이는 것입니다.


이제 Collection view의 셀 사이즈를 변경하고 격자로 만듭니다.


컬렉션뷰를 가장자리에 위치합니다 (sectionSetReference ==.fromContentInset). UICollectionViewFlowLayout 이 사용되었고 수직 으로 스크롤 가능합니다.

가로방향에서 셀들이 노치에 덮여지는걸 볼수 있습니다. 이것을 고치려면 section content insets에 safe area insets을 추가할수 있지만 iOS 11 에서UIColelctionViewFlowLayout는 다음과같은 새로운 sectionInsetReference속성을 가집니다.

@available(iOS 11.0, *)
public enum UICollectionViewFlowLayoutSectionInsetReference : Int {    
    case fromContentInset        //default value
    case fromSafeArea
    case fromLayoutMargins
}

@available(iOS 11.0, *)
open var sectionInsetReference: UICollectionViewFlowLayoutSectionInsetReference

원하는 결과를 얻으려면 fromSafeArea를 값으로 설정하면 됩니다. 이 경우 실제 색션 컨텐츠 인셋은 section content insets + safe area insets입니다.


sectionInsetReference ==.fromSafeArea

비슷하게 fromLayoutMargins값이 사용되었을때 collection view layout margins이 section content insets으로 추가되어집니다.


sectionInsetReference == .fromLayoutMargins


Recap

iOS 11에서 에플이 추가한 유용한 도구인 safe area를 다루어 봤습니다. 해당 내용들을 요약하고 설명하려고 노력했습니다. 다음 참조들을 통해서 추가적인 정보를 얻는것도 좋습니다.

Updating Your App for iOS 11
What’s New in Cocoa Touch
Building Apps for iPhone X
Positioning Content Relative to the Safe Area
Designing for iPhone X