WWDC23 - What's new in UIKit / SwiftUI

백상휘·2023년 6월 8일
0

SwiftUI, UIKit 에 새로운 기능들이 무엇인지 알아본다.

UIKit

Key features

Xcode previews

Xcode previews 는 사진 한장으로 정리할 수 있겠다.

UIViewController 뿐 아니라 UIView 도 가능하다.

View controller lifecycle updates

라이프 사이클 함수가 하나 추가된다.

UIViewController.viewIsAppearing(_:)

이 함수는 viewWillAppear(_:)viewDidAppear(_:) 사이에 호출된다.

viewIsAppearing(_:) 은 뷰가 화면에 보이기 직전 UI 를 업데이트 하기 좋은 함수이다. 이 시점을 정확히 말하자면,

  • 뷰 컨트롤러와 뷰의 traitCollection 이 업데이트 되었음.
  • 뷰가 슈퍼뷰에 추가되어 표시됨.

이렇게 표현할 수 있겠다. viewIsAppearing(_:) 내부에서는 위치와 크기 등을 지정하는 코드에 적합한 것이다.

viewIsAppearing(_:) 은 iOS 13 부터 사용 가능하다.

viewWillAppear(_:) 는 크기를 정하거나 위치를 정하기에 너무 빠르다. 뷰들이 막 슈퍼 뷰에 포함되기 시작하는 시점이기 때문이다. viewDidAppear(_:) 는 Transaction animates 후에 수행된다. 필자도 viewDidAppear(_:) 에서 뭔가 뷰 업데이트를 약간 한박자 느린 감각을 느꼈다.

이런 문제때문에 layoutIfNeeded() 를 사용한 경험이 있는데 viewIsAppearing(_:) 이 새로운 해답이 될 수 있을지는 모르겠다. viewIsAppearing(_:)viewWillAppear(_:) 과 같은 Transaction 시점에 해당되므로 사용자에게 더욱 자연스러운 뷰를 제공할 수 있을 것이다.

참고로 캡처에 viewWillLayoutSubviews(), viewDidLayoutSubviews() 를 확인할 수 있다. 이 두 개의 콜백은 어떤 뷰가 layoutSubviews() 를 실행할 때 같이 실행되는 것인데, 새로 나온 viewIsAppearing(_:) 는 뷰 컨트롤러의 라이프 사이클에서 한번만 실행된다.

어쨋든 써 봐야 알 것 같다.

Trait system enhancements

iOS 17 부터 trait system 이 개선된다.

이제는 UITraitCollection 에 커스텀 trait 을 저장할 수 있다. 여기에는 데이터를 저장할 수도 있다.

trait 의 변화를 서브클래싱으로 구현한 traitCollectionDidChange 으로 캡처할 수도 있다.

이런 커스텀 trait 들은 SwiftUI 에서도 사용할 수 있다.

자세한 내용은 'Unleash the UIKit trait system' 에서 다룬다고 한다.

Animated symbol images

말 그대로다. SF Symbol 에서 확인할 수 있는 아이콘들은 정적인 상태인데, 애니메이션을 추가할 수 있다.

// 움찔하는 애니메이션. 한번만 실행됨.
imageView.addSymbolEffect(.bounce)

// 여러 색상이 반복되는 애니메이션. 멈춤 실행하기 전까지 반복.
imageView.addSymbolEffect(.variableColor.iterative)

// 위의 애니메이션 중지.
imageView.removeSymbolEffect(ofType: .variableColor)

// 다른 심볼로 바꿀 때 애니메이션 추가. 한번만 실행됨.
imageView.setSymbolImage(pauseImage, contentTransition: .replace.offUp)

공식문서를 찾아보니 iOS 17 부터 가능한 것으로 확인되었다. (ㅠㅠ)

위의 3 기능 말고도 많은 애니메이션이 준비되어 있다고 한다. 자세한 내용은 'Animate symbols in your app' 에서 다룬다고 한다.

Empty states

처음에는 상태기반의 아키텍처같은 얘기가 나오는건가 했는데 정말 화면에 출력할 것이 없을 때 보여주는 화면을 얘기하는 것이었다.

이 화면은 일종의 제한 상황을 대신 표현하는 것인데 발표에서는 예로 '인터넷 연결상태' 라고 하였다.

UIContentUnavailableConfiguration 을 통해 placeholder 컨텐츠(텍스트, 이미지, 설명)를 표시할 수 있다.

이 객체를 viewController 에 할당하고 있다.

이런 설정을 어디서 하면 좋을까? viewDidLoad ?? 새로운 메소드가 또 등장한다.

updateContentUnavailableConfiguration(using:) 을 이용해서 UIContentUnavailableConfiguration 을 설정하고 할당한다. 이번에는 '데이터 없음' 상태를 표시하는데, searchResults 프로퍼티가 비어있다면 Empty state 화면을 표시하도록 하고 있다.

보시다시피 query를 searchResults 에 반영한 뒤 setNeedsUpdateContentUnavailableConfiguration() 을 호출하여 결과값에 따라 화면을 다르게 설정할 수 있다.

참고로 이 기능도 iOS 17 부터 가능하다.

Internationalization

우선 이 얘기를 하기 전에 Font 에 대한 용어 정리부터 진행한다.

  • x-height = 소문자 위에 위치하는 임의의 선
  • baseline = 모든 문자들이 공통적으로 깔고 앉아 있는 선
  • line-height = baseline 과 baseline 사이의 높이
  • Ascenders = x-height 선 위에 튀어나오는 부분
  • Descenders = basline 선 밑에 튀어나오는 부분

이런 구분만 있다면 화면에 텍스트를 렌더링하는 건 문제가 없...지 않다. 위의 예시는 영어인데 아랍어, 힌두어 등은 영어와 달리 Ascender / Descender 가 많이 두드러진다.

Ascender / Descender 가 많이 두드러지면 텍스트가 겹칠 수 있다.

그러므로 UILabel 등의 텍스트 요소들이 언어를 인식하여 다양한 구조를 갖도록 조정한다. 이는 사용자에 따라 자동으로 진행될 것이다.

언급을 하지 않아서 잘 모르겠는데 아무래도 사용자 지역 설정에 따라 달라지는 것 같다. 이렇게 생각한 이유는 아래에 일부 텍스트 요소를 설정하는 부분을 보고 유추한 것이다.

만약 한국어 컨텐츠들 사이에 태국어를 표시해야 할 경우 이처럼 trait 설정을 진행하면 된다. 이번에 새로 나온 typesettingLanguage (iOS 17) 프로퍼티를 이용하면 되는 것이다.

SF symbol 을 통해 여러 버전의 이미지를 보여주는 기능도 추가되었다. Locale 을 이용해서 SF symbol 을 컨트롤 하는 코드 예제이다.

자세한 사항들은 'What's new with text and text interactions' 에서 다룬다고 한다.

Improvements for iPad

Stage Manager 를 얘기한다. 앱 하나만을 화면에 띄우는 것인데 개인적으로 블로그 포스팅 긴 것을 각 잡고 읽어야 할 때 자주 사용한다.

iPad 에서도 Stage Manager 가 가능하다. 필자의 iPad 는 애플 실리콘 버전이 아니라 안 되지만 애플 실리콘이 들어간 아이패드 에어, 프로 등은 가능할 것이다.

이 Stage Manager 를 이용해 띄운 창을 드래그로 여기저기 이동하고 크기를 조정할 수 있다는 것이다.

이 기능을 적용하는 방법은 2 가지다.

  • UINavigationBar 를 사용할 경우 UINavigationBar 에 PanGesture 를 적용한다.
  • UINavigationBar 가 없을 경우 UIWindowSceneDragInteraction 을 사용한다.

이 발표에서는 컬럼 스타일의 UISplitViewController 를 추천했다. 만약 창의 크기가 줄어들면 SideBar 가 사라지고 필요할 때마다 임시로 불러올 수 있다.

UISplitViewController 의 preferredDisplayMode, preferredSplitBehavior 를 오버라이드 하여 임의로 SideBar 의 behavior 를 조정할 수도 있다.


이번 iOS 17 에 새로 나온 UIDocumentViewController는 자동으로 타이틀을 세팅해주거나, 공유, 드래그 앤 드랍, 키 명령 등을 시스템 기반으로 지원한다. UIDocument 객체가 UINavigationItemRenameDelegate 를 구현하고 있기 때문에 rename UI/UX 도 향상 되었다.

자세한 사항은 'Build better document-centric apps' 에서 다룬다.


새로운 iPad Pro 와 iOS 16.4(왜 iOS?) 를 통해 애플 펜슬의 hover 제스처를 소개한 바 있다(난 몰랐다...). 이는 UIHoverGestureRecognizer 덕분이다.

zOffset 프로퍼티를 통해 화면과 펜슬의 거리를 0~1 로 일반화하고, altitudeAngle 과 azimuthAngle 프로퍼티로 어떤 뷰가 그려질지 프리뷰로 보여준다.

이와 더불어 PencilKit 의 ink 도 추가되었다.

이러한 ink 는 PKDrawing 데이터를 포함하는데 이전 버전의 iOS 에서는 불러올 수 없다.

PKDrawing, PKStroke 등 여러 데이터 모델의 버전을 통해 iOS 버전을 맞춰야 한다. 1 버전일 경우 14 버전, 2 버전일 경우 17 버전임을 유념하도록 하자.


iOS 17 부터 UIScrollView 의 Page Down, Page Up, Home, End 등의 스크롤 기능을 키보드에서 사용 가능하다. UIScrollView 의 allowsKeyboardScrolling API 를 이용하자.

General enhancements

Collection view improvements

뭔가 했는데 진짜 성능이 좋아졌다.

원래 UITableView, UICollectionView 등의 성능 저하를 논하는 블로그 포스팅은 많았다. 내부 셀을 재사용하는 과정에 새로운 오토레이아웃 엔진 실행 비용이 들어가며 비용이 천정부지로 올라가는 것인데, 이를 해결하기 위해 FlexLayout 같은 라이브러리를 사용하기도 했다.

이번 iOS 17 부터 굉장한 양의 데이터를 통해 테스트한 바, 50% 이상의 성능 향상이 확인되었다고 한다.

물론 이건 해봐야 안다.

CollectionView 를 이용해 Grid Layout 형식으로 셀을 쉽게 배치하는 UICompositionalLayout 에도 개선사항이 있다.

NSCollectionLayoutDimension.estimated 속성을 사용한 것인데, 두 셀의 높이가 맞지 않는다.

NSCollectionLayoutDimension.uniformAcrossSiblings 가 추가되었다. 가장 큰 셀을 기준으로 레이아웃 크기를 설정한다. 모든 상황에 들어맞는 것은 아니므로 개발자의 선택이 필요하다.

Spring animation parameters

Spring animation 을 만들 수 있다. Spring animation 에는 2 개의 파라미터가 있다.

  • duration : spring animation 이 시작되기까지의 시간
  • bounce : spring animation 이 실행되는 시간

즉, duration 은 spring animation 과 직접적인 관계가 있는 것은 아니다.

// Using the new UIView spring animation API
UIView.animate (springDuration: 0.5, bounce: 0.0) {
	circle.center.x += 100
}

// 파라미터는 옵셔널
UIView.animate {
	circle.center.x += 100
}

더 자세한 사항은 'Animate with Springs' 를 참고할 수 있다.

Text interactions

커서, 텍스트 선택, selection loupe (손가락을 대고 있으면 텍스트 뷰의 특정 부분이 확대되는 기능) 에 대한 개선사항이 있었다.

워드 프로세스 개발자들은 시스템 기반의 뷰를 가져다 쓸 수 있다. 여기서는 UITextInteraction 을 직접 만들 필요가 없다.

UITextView 는 더욱 커스터마이징 할 수 있도록 개선되었다. UITextViewDelegate 를 이용하여 사용자 반응이나 텍스트에 대한 메뉴(링크 혹은 텍스트 부착물 등에 관한) 에 대한 개선도 이루어졌다.

자세한 사항은 'What's new with text and text interaction' 을 확인하기 바란다.

Default status bar sytle

아이폰 노치 부분의 시간이나 와이파이 상태 등을 알려주는 부분에 대한 개선이 이뤄졌다.

status bar 와 겹치는 컨텐츠에 의해 색이 변한다.

Drag and drop enhancements

다운로드 받을 수 있는 파일이나 이미지를 드래그 앤 드랍으로 홈 스크린에 끌어와서 다른 파일을 열 때 사용할 수 있다.

CFBundleDocumentTypes 값을 Info.plist 파일에서 찾아 파일 타입을 설정할 수 있다. 앱이 열릴 때는 UISceneDelegate 메소드를 사용한다.

ISO HDR image support

UIKit 에서 ISO HDR 이라는 종류의 이미지를 지원한다고 한다.

'Support HDR images in your app' 에서 자세히 다룬다.

Page Control

UIPageControl 에 정해진 duration 동안 슬라이드 쇼를 보여주는 기능이 추가되었다.

UIPageControlProgress, UIPageControlTimerProgress 를 이용하여 사진 뿐 아니라 비디오까지도 슬라이드 쇼 형태로 보여줄 수 있다.

// Setting up a UIPageControlTimerProgress
let timerProgress = UIPageControlTimerProgress (preferredDuration: 10)
pageControl.progress = timerProgress

timerProgress.resumeTimer()

Palette menus

iOS 17 뿐 아니라 macOS Sonoma 에서 이번에 새로 Palette Menu 가 추가되었다.

인상적이군...

이런 메뉴를 추가하고 싶다면 아래와 같이 displayAsPalette 를 추가하면 된다.

UIMenu(options: [.displayInline, .displayAsPalette], children: [...])

SwiftUI

SwiftUI in more places

ARKit, RealityKit 이 사용되는 visionOS 를 얘기하는 것이다.

visionOS 에서 보여주는 화면에는 3 개의 구성요소가 있다.

  • window : visionOS 에서 보이는 2차원의 화면
  • volume : 화면에 물체와 같은 부피를 넣어서 만지고 움직일 수 있도록 함
  • space : 위의 둘을 이용해 사용자가 경험하는 전체 화면

window 를 만드는 건 아래와 같이 한다.

WindowGroup 안에 아래와 같이 여러 컨테이너 뷰를 넣으면 된다.

여기에 volume 을 추가하고 싶다면 WindowGroup 에 viewModifier 를 적용하면 된다.

자세한 사항은 'Meet SwiftUI for spatial computing' 을 참고하라.

WatchOS 10 에도 상당한 개선이 있었는데, 전체적으로 컨테이너 뷰마다 다른 뷰를 제공하여 더욱 사용자 경험을 개선하였다.

배경에도 많은 개선이 있었는데 컨텐츠에 따라 들어가고 나오는 배경화면을 containerBackground 로 만들 수 있다.

CityDetails(city: city)
	.containerBackgroiund(for: .navigation) {...}

또한 툴 바 아이템들도 아래와 같이 추가되었다.

이 다음은 DataPicker, List, Widget, MapKit, Swift Charts, In-app purchasing 에 대한 내용이 주루룩 나온다. 대부분이 Less code more outcomes 였으므로 따로 언급하지는 않도록 하겠다.

Simplified data flow

@Observable 매크로가 소개되었다.

@Observable
class Dog: Identifiable {
	var id = UUID()
    var name = ""
    var age = 1
    var breed = DogBreed.mutt
    var owner: Person?
}

만약 위의 Dog 객체를 뷰에 적용시키기만 하면 뷰는 해당 객체의 변화에 반응하여 뷰를 업데이트할 것이다. 이전에 @ObservableObject, @ObservedObject 를 썻던 것에 비해 훨씬 간단해졌다.


이제는 뷰에 @State, @Environment 만을 사용하여 상태값을 표시할 수 있게 되었다.

@Observable 한 객체를 Read-Write 하게 만들기 위해 @State 를 사용할 수 있다.

또한, Environment 로 전달된 객체를 Environment dynamic property 로 받아올 수 있다.

@main
struct WhatsNew2023: App {
	@State private var currentUser: User?
	
    var body: some Scene {
		WindowGroup {
			ContentView()
				.environment (currentUser)
		}
	}
}

@Observable final class User { ... }


struct ProfileView: View {
	// Environment dynamic property. custom key 도 가능.
	@Environment (User.self) private var currentUser: User?
	
    var body: some View {
		if let currentUser {
			UserDetails (user: currentUser)
		} else {
			LoginScreen ()
	}
}

자세한 내용은 'Discover Observation with SwiftUI' 에서 더 다룰 예정이라고 한다.

SwiftData 는 새롭게 나온 데이터 저장 프레임워크다. 객체를 데이터 모델로 쉽게 만들 수 있다.

// SwiftData model
import Foundation
import SwiftData

@Model
class Dog {
	var name = ""
	var age = 1
	var breed = DogBreed.mutt
	var owner: Person?
}

위의 @Observable 예제에서 @Model 부분만 변경한 것이다. 이렇게 해도 @Observable 의 기능은 사라지지 않는다.

이제 데이터를 저장하고 불러올 때 어떻게 하는지 알아보자.

import SwiftUI
import SwiftData

@main
struct WhatsNew2023: App {
	var body: some Scene {
		WindowGroup {
			ContentView()
		}
		.modelContainer(for: Dog.self)
	}
}

우선 modelContainer 를 이용하여 모델 컨테이너와 모델 타입을 정의한다.

import SwiftUI
import SwiftData

struct RecentDogs: View {
	@Query(sort: \.dateSpotted) private var dogs: [Dog]
	
	var body: some View {
		ScrollView {
			LazyVStack {
				ForEach (dogs) { dog in
					DogCard (dog: dog)
				}
			}
		}
    }
}

@Query 를 이용하여 모델 데이터를 불러온다. 위와 같이 sort 를 할 수도 있고 저런 파라미터를 전달 안하면 모두 불러온다.

'Meet SwiftData' 에서 자세한 정보를 더 확인할 수 있다.

이 외엔 Inspector, Dialog, Table, List 에 대한 설명이 주루룩 나온다. 필요할 때마다 찾아서 다뤄도 될 것 같으므로 여기서는 이만 줄이도록 하겠다.

Extraordinary animations

KeyframeAnimation 을 통해 뷰에 여러가지 프로퍼티를 한번에 반영하여 애니메이션을 실행하도록 할 수 있다.

애니메이션이 가능한 프로퍼티를 가진 객체와 상태값을 전달한 뒤, 뷰를 생성하는 클로저를 정의하고, 다음 클로저에서 앞에 전달한 프로퍼티가 어떤 방식으로 바뀔지 정의한다.

KeyframeAnimator(
	initialValue: LogoAnimationValues (), trigger: runPlan
) { values in
	LogoField(color: color, isFocused: isFocused)
		.scaleEffect(values.scale)
		.rotationEffect(values.rotation, anchor: bottom)
		.offset(y: values.verticalTranslation)
} keyframes: { _ in
	KeyframeTrack(\.verticalTranslation) {
		SpringKeyframe(30, duration: 0.25, spring: smooth)
		CubicKeyframe(-120, duration: 0.3)
		CubicKeyframe(-120, duration: 0.5)
		CubicKeyframe(10, duration: 0.3)
		SpringKeyframe(0, spring: bouncy)
	}

	KeyframeTrack(\.scale) { ... }
	KeyframeTrack(\.rotation) { ... }
}

'Wind your way through advanced animations in SwiftUI.' 에서 더 자세한 사항을 알아볼 수 있다.

상태값에 따라 다른 애니메이션을 실행할 수도 있다.

// Phase animator
HappyDog ()
	.phaseAnimator (
		SightingPhases. allCases, trigger: sightingCount
    ) { content, phase in
		content
			.rotationEffect (phase.rotation)
			.scaleEffect(phase.scale)
	} animation: { phase in
		switch phase {
			case .shrink: snappy (duration: 0.1)
			case .spin: .bouncy
			case .grow: spring(duration: 0.2, bounce: 0.1)
            case .reset: .linear(duration: 0.0)
		}
	}

햅틱 피드백에 대한 얘기도 나왔다. viewModifier 형태로 제공되는데 아래와 같이 사용할 수 있다.

.sensoryFeedback(.increase, trigger: sightingCount)

이외에도 visualEffect, foregroundStyle, symbolEffect 에 대한 얘기가 나왔으나 바로 적용할 부분은 아니어서 여기서 줄이기로 하였다.

Enhanced interactions

앱의 사용자 경험 증가를 위해 ScrollView 의 scrollTransition, containerRelativeFrame, scrollTargetLayout, scrollTargetBehavior 을 소개한다.

의미있다고 생각한 부분은 scrollPosition 인데, 스크롤 컨텐츠의 특정 데이터 ID 를 binding 객체로 넘기면 해당 데이터가 표시되도록 스크롤을 조정해준다.

접근성에 대한 문제도 다루었다. 'Build accessible apps with SwiftUI and UIKit' 에서 찾아볼 수 있도록 하였다.

이외에도 여러 개선사항이 있었으나 워낙 짧게 다뤘으므로 이만 줄이기로 하였다.

References

profile
plug-compatible programming unit

0개의 댓글