iOS 27의 SwiftUI 새 기능
SwiftUI 릴리스마다 Apple이 무엇을 다시 만들기로 했는지를 보면 프레임워크의 압력점이 어디였는지 알 수 있습니다. iOS 27의 답은 유난히 폭넓습니다. 리스트는 일급 재정렬 기능을 얻고, 문서는 새로운 reader/writer 프로토콜 계열을 얻으며, 툴바는 명시적 우선순위를 가진 오버플로 모델을 얻고, 오류 표시는 마침내 Error를 직접 넘길 수 있는 바인딩을 얻습니다. 이번 릴리스는 네 가지 영역을 한꺼번에 움직이며, 대부분의 앱은 그중 최소 두 가지를 건드립니다.1
이렇게 폭넓은 릴리스에서는 모든 것을 채택하고 싶은 유혹이 생깁니다. 더 나은 선택은 어떤 추가 기능이 빌드 방식을 바꾸는지, 그리고 어떤 것이 이미 쓰고 있는 패턴에 대한 편의 오버로드인지를 구분하는 것입니다. 드래그 재정렬과 문서 프로토콜은 전자입니다. 손으로 작성하던 코드를 대체합니다. 항목 기반 알림과 AsyncImage(request:)는 후자입니다. 우회책을 제거합니다. 이 글은 iOS 27의 SwiftUI 영역을 이런 갈래로 정리하면서, 실제 선언과 각 기능이 코드에서 제 역할을 하는 시점에 대한 근거를 함께 다룹니다.
세션 269에서 Apple은 이번 릴리스를 단일 대표 기능이 아니라, 다듬어진 룩 앤 필, 강력한 새 문서 API, 새로운 상호작용 방식, 성능 작업이라는 네 가지 폭넓은 갈래가 함께 움직이는 것으로 설명합니다.35
TL;DR / 핵심 요약
- 리스트와 커스텀 컨테이너에 선언적 재정렬이 추가됩니다.
reorderContainer(for:isEnabled:move:)는 컨테이너를 표시하고,DynamicViewContent의reorderable()은 행을 참여시키며, 인덱스 계산을 손으로 작성하는 대신ReorderDifference를 받습니다.23 - 지연(lazy) 드래그 컨테이너는
dragContainer(for:itemID:in:_:)와draggable(containerItemID:containerNamespace:)를 통해 도입됩니다. 식별자만 전달하므로 드래그가 시작될 때 프레임워크가 페이로드를 지연 방식으로 가져옵니다.45 - 새 문서 모델은
ReadableDocument와WritableDocument로 등장합니다(디스크 작업은DocumentReader/DocumentWriter가 담당하고, 단순한 경우에는FileWrapperDocumentReader/FileWrapperDocumentWriter가 맡으며),URLDocumentConfiguration이 이를 뒷받침합니다.6789101112 - 툴바는
ToolbarOverflowMenu,topBarPinnedTrailing배치,visibilityPriority(_:)를 얻으므로, 바에 공간이 부족해질 때 어떤 컨트롤을 살릴지 직접 결정합니다.131415 - 오류 표시에는
alert(error:actions:message:)와 항목 기반alert(_:item:actions:)/confirmationDialog가 추가되고, 전체URLRequest를 제어하는AsyncImage(request:)와 세션을 공유하는asyncImageURLSession(_:)도 추가됩니다.1617181920 - 그 밖에 영역을 마무리하는 것으로
swipeActions(...onPresentationChanged:),swipeActionsContainer(),NavigationTransition.crossFade,TabRole.prominent,UIHostingSceneDelegate,GestureInputKinds가 있습니다.212223242526
드래그 재정렬이 선언적으로 바뀌다
SwiftUI에서 리스트를 재정렬하려면 예전에는 onMove, 인덱스 집합, 그리고 자신의 모델 변경으로 변환해야 하는 목적지 오프셋이 필요했습니다. iOS 27은 이를 두 부분으로 된 선언으로 대체합니다. 컨테이너를 재정렬 가능하다고 표시하고, 그 콘텐츠를 참여하는 것으로 표시합니다. 그러면 컨테이너가 구조화된 diff를 넘겨줍니다. 그 diff가 변경하는 모델은 iOS 27에서 그 자체로 더 잘 관찰됩니다. SwiftData는 같은 릴리스에서 일급 관찰 및 영속 히스토리 API를 얻었으므로, 한 뷰에서 재정렬한 스토어는 그것이 읽히는 모든 곳에서 동기화 상태를 유지합니다.
단일 컬렉션 경우가 가장 흔합니다. 컨테이너에 reorderContainer(for:isEnabled:move:)를 선언하고, 그 안의 DynamicViewContent에 reorderable()을 선언합니다.23
struct LandmarkList: View {
@State private var landmarks: [Landmark]
var body: some View {
List {
ForEach(landmarks) { landmark in
LandmarkRow(landmark: landmark)
}
.reorderable()
}
.reorderContainer(for: Landmark.self) { difference in
// Apply the reorder to your model.
landmarks.apply(difference)
}
}
}
시그니처가 계약을 알려줍니다. 컨테이너 수정자는 Item : Identifiable에 대해 제네릭이며 ReorderDifference<Item.ID, ReorderableSingleCollectionIdentifier>를 제공합니다.2
nonisolated func reorderContainer<Item>(
for item: Item.Type,
isEnabled: Bool = true,
move: @escaping (ReorderDifference<Item.ID, ReorderableSingleCollectionIdentifier>) -> ()
) -> some View where Item : Identifiable, Item.ID : Sendable
여기서 두 가지 설계 결정이 중요합니다. 첫째, 프레임워크가 상호작용을 주도합니다. Apple 문서가 설명하듯이, 재정렬 가능한 항목은 드래그 제스처로 들어올릴 수 있고, 플레이스홀더 뷰가 그 자리를 차지해 항목이 놓일 위치를 보여주며, 플레이스홀더는 컨테이너 안에서 드래그를 따라갑니다.2 더 이상 그 어포던스를 직접 만들지 않습니다. 컬렉션을 기술하고 결과에 반응할 뿐입니다. 둘째, isEnabled는 별도의 수정자가 아니라 매개변수이므로, 편집 모드 토글이 조건부 뷰 구성이 아니라 하나의 불리언이 됩니다.
컨테이너가 둘 이상의 컬렉션을 담을 때는 컬렉션 식별자 타입을 받는 오버로드인 reorderContainer(for:in:isEnabled:move:)를 사용합니다.27
nonisolated func reorderContainer<Item, CollectionID>(
for item: Item.Type,
in collectionID: CollectionID.Type,
isEnabled: Bool = true,
move: @escaping (ReorderDifference<Item.ID, CollectionID>) -> ()
) -> some View where Item : Identifiable, CollectionID : Hashable, CollectionID : Sendable, Item.ID : Sendable
이제 ReorderDifference는 항목 ID와 컬렉션 ID 양쪽으로 키가 지정되므로, 한 섹션에서 다른 섹션으로 넘나드는 이동을 표현할 수 있습니다. Apple의 안내는 명확합니다. 컨테이너에 여러 컬렉션이 있을 때는 다중 컬렉션 오버로드를 사용하고, 하나만 있을 때는 단일 컬렉션 편의 메서드를 사용하라는 것입니다.27 reorderable() 수정자는 모호함을 해소해야 할 때 일치하는 컬렉션 식별자를 받습니다.3
ForEach에 reorderable()을, 그 부모에 reorderContainer를 추가해 재정렬을 활성화하고, 클로저에서 difference를 처리합니다.
세션 271에서 Apple은 동일한 재정렬 코드가 List에서 LazyVGrid로 변경 없이 그대로 옮겨지는 것을 보여줍니다. 수정자가 컨테이너가 아니라 컬렉션을 기술하기 때문에, 드래그 앤 드롭을 지원하는 어떤 컨테이너에서든 재정렬이 동작합니다.36
지연(lazy) 드래그 컨테이너
iOS 27 드래그 이야기의 나머지 절반은 비용에 관한 것입니다. 기존 draggable은 페이로드를 미리 만들어 둘 것을 요구하므로, 드래그가 시작되기도 전에 프레임워크가 항목을 구체화하고(때로는 렌더링까지) 해야 할 수 있습니다. 수천 개의 행으로 이루어진 지연 리스트에서는 이것이 낭비되는 작업입니다.
dragContainer(for:itemID:in:_:)는 드래그 가능한 뷰들의 컨테이너를 정의하고, 드래그된 식별자들에 대한 클로저로서 페이로드를 단 한 번만 요청합니다.4
nonisolated func dragContainer<ItemID, Item, Data>(
for itemType: Item.Type = Item.self,
itemID: KeyPath<Item, ItemID>,
in namespace: Namespace.ID? = nil,
_ payload: @escaping (Array<ItemID>) -> Data
) -> some View where ItemID : Hashable, ItemID : Sendable, Item : Transferable, Item == Data.Element, Data : Collection
그 컨테이너 안에서 각 드래그 가능한 자식은 항목의 식별자만 전달하는 draggable(containerItemID:containerNamespace:)를 사용합니다.5
nonisolated func draggable<ItemID>(
containerItemID: ItemID,
containerNamespace: Namespace.ID? = nil
) -> some View where ItemID : Hashable, ItemID : Sendable
이것이 큰 컬렉션에 더 나은 기본값인 이유는 Apple 자신의 설명에 있습니다. 이 수정자는 페이로드가 아니라 식별자만 제공하므로 지연 방식으로 동작하고, 따라서 프레임워크는 드래그가 시작될 때만 실제로 드래그된 항목을 요청하며, 페이로드에 접근하기 위해 뷰를 렌더링할 필요가 없습니다.5 이름으로 자신을 식별하는(그리고 Identifiable을 절대 준수하지 않는) Fruit 값도 여전히 다중 항목 드래그의 소스가 될 수 있는데, 컨테이너가 Identifiable 준수가 아니라 여러분이 제공하는 KeyPath를 기준으로 키를 지정하기 때문입니다.4
크로스 플랫폼 코드에서 짚어둘 점이 있습니다. dragContainer와 draggable(containerItemID:containerNamespace:)는 macOS 26.0에서 사용 가능한데, 이번 릴리스의 나머지 대부분은 macOS 27.0이므로, 지연 드래그 API는 Mac에서 이미 채택할 수 있었던 것입니다.45
새로운 문서 모델
DocumentGroup과 FileDocument는 수년간 SwiftUI의 문서 앱을 떠받쳐 왔지만, 읽기 측면과 쓰기 측면이 하나의 준수(conformance)에 뒤엉켜 있었습니다. iOS 27은 이를 분리합니다. 이제 읽기와 쓰기는 별도의 프로토콜이고, 디스크 로직은 자체 계층이며, 읽기-쓰기 타입은 둘을 조합합니다.
두 개의 최상위 프로토콜은 ReadableDocument와 WritableDocument입니다.67
protocol ReadableDocument : AnyObject
protocol WritableDocument : AnyObject
읽기 전용 타입은 ReadableDocument만 준수합니다. 읽기-쓰기 타입은 둘 다 준수하며, Apple은 둘을 묶은 Document 타입 별칭(typealias)을 제공하므로 각각을 일일이 명시하지 않고도 한 쌍을 채택할 수 있습니다.67 둘 다 클래스 바운드(: AnyObject)인데, 이는 이 모델에서 문서가 참조 타입이라는 점을 드러내는 가시적 신호입니다.
실제 디스크 I/O는 reader와 writer 추상화인 DocumentReader와 DocumentWriter로 옮겨가며, 각각은 문서가 직렬화하는 스냅샷 타입으로 매개변수화됩니다.89
protocol DocumentReader<Snapshot>
protocol DocumentWriter<Snapshot>
대부분의 앱은 이것을 직접 구현하지 않습니다. 커스텀 로직이 필요 없는 작거나 중간 크기의 문서를 위해, SwiftUI는 FileWrapperDocumentReader와 FileWrapperDocumentWriter를 제공하며, 각각은 파일 래퍼로 뒷받침되고 Apple은 이를 단순한 경우에 효율적인 선택으로 설명합니다.1011
struct FileWrapperDocumentReader<Snapshot>
struct FileWrapperDocumentWriter<Snapshot>
열린 문서를 하나로 묶는 것은 URLDocumentConfiguration으로, 열린 문서의 설정과 속성을 담는 메인 액터 클래스입니다.12
@MainActor final class URLDocumentConfiguration
내보내기는 writer가 URL을 대상으로 하는 WritableDocument를 받는, 업데이트된 fileExporter를 거칩니다.28
nonisolated func fileExporter<D>(
isPresented: Binding<Bool>,
document: D?,
contentType: UTType? = nil,
defaultFilename: String? = nil,
onCompletion: @escaping (Result<URL, any Error>) -> Void,
onCancellation: (() -> Void)? = nil
) -> some View where D : WritableDocument, D.Writer.Destination == URL
D.Writer.Destination == URL 제약이 핵심을 담당합니다. 익스포터는 writer가 URL에 쓰는 쓰기 가능 문서만 받는데, 이것이 바로 시스템 다이얼로그가 처리하는 디스크 파일 경우입니다. Apple은 수명 주기를 정확히 문서화합니다. 다이얼로그는 document가 nil이 아닐 때만 나타나고, onCompletion이 실행되기 전에 isPresented가 false로 설정되며, 사용자가 취소하면 isPresented가 false로 설정되고 onCancellation이 호출됩니다.28 읽기 가능과 쓰기 가능 사이의 분리 덕분에, 뷰어 전용 기능은 쓰기 측면을 전혀 준수하지 않고도 문서를 가져올 수 있습니다.
무엇이 살아남을지 결정하는 툴바
툴바는 공간이 부족해집니다. 컴팩트 너비의 iPhone, 크기를 줄인 Mac 창, 활성화된 검색 필드는 모두 보유한 컨트롤보다 적은 슬롯만 남길 수 있습니다. iOS 27 이전에는 프레임워크가 제거 결정을 대신 내렸습니다. 이제는 여러분이 내립니다.
ToolbarOverflowMenu는 명시적인 오버플로 표면입니다. Apple은 이를 툴바 모드, 플랫폼, 커스터마이즈 가능성과 무관하게 항상 툴바의 오버플로 메뉴에 배치되는 액션으로 설명하며, iOS와 visionOS에서는 그 콘텐츠가 내비게이션 바의 오버플로 메뉴에 들어갑니다.13
nonisolated struct ToolbarOverflowMenu<Content> where Content : View
오버플로에 저항해야 하는 컨트롤을 위해, 새로운 topBarPinnedTrailing 배치는 항목을 툴바의 후행(trailing) 가장자리에 고정합니다.14
static let topBarPinnedTrailing: ToolbarItemPlacement
Apple이 문서화한 미묘한 점은, 고정된 항목은 검색이 활성화되고 공간이 충분하지 않을 때만 오버플로 메뉴로 이동하며, iOS와 visionOS에서 톱 바는 내비게이션 바라는 것입니다.14 따라서 topBarPinnedTrailing은 검색이 강제하지 않는 한 절대 묻히길 원치 않는 한두 개의 컨트롤을 위한 것입니다.
선택이 절대적이 아니라 상대적일 때는, ToolbarContent의 visibilityPriority(_:)가 항목의 순위를 매겨 프레임워크가 제거 순서를 알게 합니다.15
@MainActor @preconcurrency
func visibilityPriority(_ priority: ToolbarItemVisibilityPriority) -> some ToolbarContent
Apple의 규칙은 이렇습니다. 툴바 공간이 제한되면, 우선순위가 낮은 항목이 우선순위가 높은 항목보다 먼저 오버플로 메뉴로 이동합니다.15 중요한 컨트롤은 후행 가장자리에 자리하면서도 창이 줄어들 때 계속 표시될 수 있습니다. topBarPinnedTrailing 및 ToolbarOverflowMenu와 함께 쓰면, 이제 우아한 단계적 축소를 위한 완전한 어휘를 갖추게 됩니다. 필수 항목은 고정하고, 나머지는 우선순위를 매기며, 항상 부차적인 것은 오버플로로 보내는 것입니다.
관련된 한 수정자는 툴바를 스크롤 동작 및 iOS 26이 도입한 Liquid Glass 크롬과 연결합니다. toolbarMinimizeBehavior(_:for:)는 스크롤에 반응한 툴바 최소화를 활성화하며, Apple은 내비게이션 바가 최소화될 때 통합된 톱 탭 바도 함께 최소화된다고 설명합니다.29
nonisolated func toolbarMinimizeBehavior(
_ behavior: ToolbarMinimizeBehavior,
for bars: ToolbarPlacement...
) -> some View
지원되는 배치는 내비게이션 바이며, 기본적으로 바가 최소화될 때 안전 영역이 조정됩니다.29 Liquid Glass 툴바를 채택했고 사용자가 읽는 동안 그것이 물러나길 원했다면, 바로 이 수정자가 그 역할을 합니다.
항목 기반 알림과 오류 표시
SwiftUI의 alert API에는 오랫동안 불리언 형태(alert(_:isPresented:))가 있었는데, 이는 표시 플래그와 함께 별도의 @State에 알림 데이터를 보관하도록 강제했습니다. iOS 27은 시트와 팝오버 API가 이미 갖고 있던 항목 기반 형태를 추가하므로, 이제 데이터 자체가 트리거가 됩니다.
항목 기반 알림은 바인딩이 nil이 아닐 때마다 표시되며, 언래핑된 값을 actions 빌더에 전달합니다.17
nonisolated func alert<A, T>(
_ title: Text,
item data: Binding<T?>,
@ContentBuilder actions: (T) -> A
) -> some View where A : View
메시지 빌더를 추가하는 짝 오버로드인 alert(_:item:actions:message:)가 있고, confirmationDialog에도 동등한 쌍이 있으므로, 동일한 항목 기반 패턴이 알림과 다이얼로그 전반에 적용됩니다.183031 각 경우에서 Apple의 계약은 동일합니다. 표시가 나타나려면 데이터가 nil이 아니어야 하고, 표시가 발생한 후 데이터에 가하는 변경은 무시됩니다.18
오류 표시 오버로드가 진정으로 새로운 기능입니다. 오류를 커스텀 구조체에 매핑하는 대신, Error를 직접 바인딩합니다.16
nonisolated func alert<E, A, M>(
error: Binding<E?>,
@ContentBuilder actions: (E) -> A,
@ContentBuilder message: (E) -> M
) -> some View where E : Error, A : View, M : View
채택할 가치를 만드는 것은 그 동작입니다. 오류 값이 nil이 아닐 때 시스템이 알림을 표시하고, 오류가 LocalizedError라면 제목은 그 오류의 errorDescription에서 추론되며, 그렇지 않으면 제목은 localized description으로 대체됩니다.16 이미 정의해 둔 LocalizedError가 이제 추가 배선 없이 자신의 알림 제목을 주도합니다. OK 액션만 필요할 때는, 더 간단한 오버로드인 alert(error:actions:)가 메시지 빌더를 생략합니다.19
nonisolated func alert<E, A>(
error: Binding<E?>,
@ContentBuilder actions: () -> A
) -> some View where E : Error, A : View
struct EditorView: View {
@State private var saveError: SaveError?
var body: some View {
Form { /* ... */ }
.alert(error: $saveError) { error in
Button("Retry") { retry() }
Button("Cancel", role: .cancel) { }
} message: { error in
Text(error.recoverySuggestion ?? "")
}
}
}
사라지는 패턴은 이렇습니다. 손으로 만든 AlertError 구조체, identifiable 래퍼, 그리고 실제 오류 타입과 알림의 데이터 소스 사이의 매핑 코드입니다. 여러분의 코드가 이미 만들어내는 Error?를 바인딩하면 됩니다.
AsyncImage가 URLRequest로 성장하다
AsyncImage는 URL 이니셜라이저와 함께 출시되었지만, 헤더, 캐시 정책, 타임아웃을 설정할 방법이 없었습니다. iOS 27의 추가 기능은 그 세 가지를 모두 담는 객체인 URLRequest를 받습니다.
가장 단순한 형태는 요청으로부터 이미지를 로드하고 표시합니다.20
nonisolated init(request: URLRequest, scale: CGFloat = 1) where Content == Image
단계(phased) 형태는 콘텐츠 클로저를 구동할 AsyncImagePhase를 제공하며, Apple은 요청을 통해 캐시 정책과 타임아웃 간격을 지정할 수 있다고 설명합니다.32
nonisolated init(
request: URLRequest?,
scale: CGFloat = 1,
transaction: Transaction = Transaction(),
@ContentBuilder content: @escaping (AsyncImagePhase) -> Content
)
흔한 “로드될 때까지 이것을 보여주고, 성공하면 저것을 보여준다”는 분할을 위한 content/placeholder 형태도 있습니다.33 세 가지 모두의 동작은 문서화된 AsyncImage 계약입니다. SwiftUI는 로드가 완료될 때까지 플레이스홀더를 보여주고, 성공 시 이미지로 교체하며, 실패 시 플레이스홀더를 유지합니다.20
짝을 이루는 수정자는 asyncImageURLSession(_:)으로, 뷰 안의 AsyncImage 인스턴스들에게 가져오기에 사용할 URLSession을 넘겨줍니다.34
nonisolated func asyncImageURLSession(_ urlSession: URLSession) -> some View
var body: some View {
List(avatars) { avatar in
AsyncImage(request: URLRequest(url: avatar.url))
.frame(width: 44, height: 44)
}
.asyncImageURLSession(authenticatedSession)
}
이 조합이 인증된 이미지 로딩에 대한 답입니다. 요청을 사용하면 Authorization 헤더나 커스텀 캐시 정책을 첨부할 수 있고, 세션 수정자를 사용하면 전체 하위 트리가 각 AsyncImage가 공유 세션으로 폴백하는 대신 구성된 하나의 URLSession(커스텀 헤더, 디스크 캐시, 프록시)을 공유할 수 있습니다. 토큰 뒤에서 아바타를 로드하는 앱에게, 이것이 동작하느냐 마느냐의 차이입니다.
함께 등장하는 것들
몇 가지 작은 추가 기능은 각각 한 줄씩 다룰 가치가 있습니다. 각각이 특정한 마찰을 제거하기 때문입니다.
swipeActions는 onPresentationChanged: 클로저를 가진 오버로드를 얻는데, 이 클로저는 행의 스와이프 액션이 보이게 될 때 true로, 사라질 때 false로 호출되므로, 액션이 표시되는 동안 행을 흐리게 하거나 주변 크롬을 업데이트할 수 있습니다.21 List가 아니라 ScrollView나 LazyVStack으로 만든 커스텀 행 레이아웃의 경우, swipeActionsContainer()는 List가 이미 자동으로 하는 방식대로 행 전반에 걸친 해제와 상호 배제를 조율합니다(List에 적용하면 무동작입니다).22
NavigationTransition.crossFade는 나타나는 뷰와 사라지는 뷰 사이를 크로스페이드하는 트랜지션입니다. 시트에 지정하면, 콘텐츠를 가리기 위해 위로 이동하는 대신 콘텐츠 위로 시트를 페이드 인합니다.23 TabRole.prominent는 지원되는 탭 바에서 한 탭에 두드러진 시각적 처리를 부여하며, Apple은 명시적인 .prominent 탭이 없으면 .search 역할 탭이 기본적으로 두드러진 처리를 받을 수 있다고 설명합니다.24
UIHostingSceneDelegate는 UISceneDelegate를 확장해 SwiftUI 씬을 브리지하며, UIKit이 준수 클래스의 정적 rootScene 속성에 선언된 SwiftUI 씬을 활성화할 수 있게 합니다.25 (이것은 여기서 대부분의 플랫폼에서 iOS 26.0으로 거슬러 올라가는 유일한 항목으로, tvOS에는 27.0 베타에서 도달합니다.25) 그 씬 배관은 보이는 것보다 더 중요한데, iOS 27이 UIKit 씬 기반 수명 주기를 강제 요구 사항으로 만들기 때문입니다. 최신 SDK로 빌드했으면서 이를 채택하지 않은 앱은 아예 실행에 실패합니다. 그리고 GestureInputKinds는 제스처가 어떤 입력 종류를 인식해야 하는지를 지정하는 옵션 집합으로, 예컨대 터치와 포인터를 구분하는 제스처의 토대입니다.26
ContentBuilder가 result builder를 통합하다
위의 추가 기능들은 API 표면입니다. 2027 사이클의 한 가지 변경은 배관에 해당하며, 여러분이 채택하는 어느 하나가 아니라 컴파일하는 모든 뷰를 건드립니다. 세션 269는 이를 대부분의 SwiftUI 개발자가 마주쳤던 오류를 통해 설명합니다. “컴파일러가 합리적인 시간 안에 이 표현식의 타입을 검사할 수 없습니다(The compiler is unable to type-check this expression in reasonable time).”37
원인은 오버로드 해석입니다. 콘텐츠를 Section, Group, ForEach로 감싸는 뷰는 컴파일러가 의사결정 트리를 따라가도록 강제합니다. Apple의 설명대로, “먼저 컴파일러는 어떤 Section 오버로드를 사용할지 선택해야 합니다. Section은 View를 만드는 빌더로 초기화될 수도 있고, TableRowContent를 만드는 빌더로 초기화될 수도 있습니다. 어느 쪽을 쓸지 알기 위해 컴파일러는 두 옵션을 모두 시도해야 합니다.” 그 분기는 중첩됩니다. “중첩된 ForEach에 대해서도 컴파일러는 각각을 시도해야 합니다. 그리고 ForEach의 빌더에는 또한 검사해야 할 자체 옵션 집합이 있습니다.” 각 계층이 경로를 곱하고, “이 각 경로를 시도하는 것이 타입 검사를 점점 더 비싸게 만듭니다.”37
해결책은 트리를 무너뜨립니다. “가장 흔한 빌더 집합이 이제 단일 이니셜라이저를 공유하며, 단 하나의 직선적인 경로만 남깁니다. 이것이 가능한 이유는 여러 다른 빌더 타입이 단일 빌더 아래 통합되었기 때문입니다. 바로 ContentBuilder입니다!”37 Apple은 이를 더 긴 흐름의 시작으로 자리매김합니다. “이것은 SwiftUI의 모든 API에 걸쳐 통합된 빌더를 가능하게 하는 단계입니다.”37
두 가지 속성 덕분에 ContentBuilder는 나중이 아니라 지금 의지할 수 있습니다. 배포 대상 비용이 없습니다. “ContentBuilder는 어떤 최소 배포 대상에서든 사용할 수 있는데, 내부적으로는 기존 ViewBuilder의 진화이기 때문입니다.”37 그리고 무엇을 출시하든 상관없이 빌드 시점에 이득이 발생합니다. “ContentBuilder는 Xcode 27로 빌드할 때 SwiftUI의 타입 검사 성능을 상당히 개선합니다. 2027 릴리스를 대상으로 하든, 이전 릴리스를 대상으로 하든 마찬가지입니다.”37 Apple 문서는 그 선언에서 하위 호환성을 확인합니다. ContentBuilder는 타입 별칭(typealias)이고, 그 가용성은 iOS 13.0과 macOS 10.15까지 거슬러 올라가는 것으로 나열되며, “클로저로부터 뷰와 기타 콘텐츠 타입을 구성하는 커스텀 매개변수 속성”으로 설명됩니다.38
Swift 엔지니어링 매니저 Holly Borla는 WWDC26 마무리 인터뷰에서 컴파일러 측면을 뒷받침했습니다. 그 오류는 “컴파일러 타입 검사기의 폴백”이라고 그녀는 설명했고, 팀은 그것이 나타나는 지점을 좁혔습니다. “올해 우리는 중첩된 클로저와 SwiftUI 뷰 바디에서 그 오류를 완화하는 데 많이 집중했는데, 그곳이 그 오류를 정말 흔하게 보는 곳이기 때문입니다.”40 그녀는 작업이 공개적으로 계속된다고 덧붙였습니다. “아직 해야 할 작업이 더 남아 있고, Open Source Swift 프로젝트를 통해 그것을 따라갈 수 있습니다.”40
SwiftUI 팀은 그룹 랩에서 이 변경에 두 번째 차원을 부여했습니다. WWDC 2026 SwiftUI Group Lab의 로컬 전사 녹음을 의역하면, 팀은 기존의 타입별 빌더 오버로드가 자신들의 API 표면을 스스로 제한해 왔다고 설명했습니다. 빌더를 추가하고 싶은 곳마다 타입 검사가 악화되었기 때문에, 그들은 물러섰고, 그 결과 ForEach 같은 것들을 원했던 것보다 더 적은 위치에서만 사용할 수 있게 두었습니다.39 통합은 그 한도를 끌어올립니다. 팀은 또한 이제 통합된 빌더를 뷰 바깥에서도 사용할 수 있어, 뷰만이 아니라 자신만의 빌딩 블록으로 SwiftUI와 유사한 커스텀 DSL을 조립할 수 있다고 언급했습니다.39 컴파일러 이득이 표제이고, 설계 여유는 더 조용한 결과입니다.
채택 우선순위
이렇게 폭넓은 릴리스는 분류를 보상합니다. 다음 것들을 먼저 잡으세요.
- 손으로 작성한 재정렬을 대체하세요.
onMove인덱스 계산을 유지하고 있다면,reorderContainer(for:isEnabled:move:)와reorderable()은 코드의 순수한 삭제이자 더 나은 상호작용입니다(플레이스홀더 어포던스가 여러분의 것이 아니라 시스템의 것입니다).23 리스트가 많은 앱에서는 재정렬 API가 이번 릴리스에서 가장 큰 비중을 차지합니다. - 오류 바인딩 알림을 채택하세요.
alert(error:actions:message:)는 실패를 표면화하는 모든 화면에서 커스텀 오류 래퍼 구조체를 제거하고, 이미 보유한LocalizedError가 이제 자신의 알림 제목을 붙입니다.16 적은 노력으로 즉각적인 가독성을 얻습니다. - 큰 드래그 소스를 지연 컨테이너로 전환하세요. 수백 개를 넘는 드래그 가능한 행으로 이루어진 어떤 리스트든
dragContainer(for:itemID:in:_:)와draggable(containerItemID:containerNamespace:)의 혜택을 보는데, 프레임워크가 결코 쓰지 않을 수도 있는 페이로드를 구체화하기를 멈추기 때문입니다.45 - 툴바에 우선순위 전략을 부여하세요. 툴바가 컴팩트 너비에서 한 번이라도 넘친다면,
visibilityPriority(_:),topBarPinnedTrailing,ToolbarOverflowMenu를 통해 프레임워크의 기본값을 받아들이는 대신 무엇이 살아남을지 직접 결정할 수 있습니다.131415 - 문서 앱은 반사적으로가 아니라 신중하게 마이그레이션하세요.
ReadableDocument/WritableDocument분리는 올바른 모델이지만, 다른 것들보다 더 큰 변경입니다. 이미 문서 계층을 건드리고 있을 때 채택하고, reader와 writer 프로토콜을 손으로 구현하는 대신 작거나 중간 크기의 경우에는FileWrapperDocumentReader/FileWrapperDocumentWriter에 의지하세요.671011
관통하는 원칙은 이렇습니다. 유지하는 코드를 삭제해 주는 추가 기능은 채택하고, 이미 잘 동작하는 코드를 재구성하는 것은 미루세요.
FAQ
iOS 27에서 SwiftUI 리스트를 어떻게 재정렬 가능하게 만드나요?
컨테이너에 reorderContainer(for:isEnabled:move:)를 선언하고, 그 안의 DynamicViewContent(보통 ForEach)에 reorderable()을 적용하세요. 컨테이너의 move 클로저는 모델에 적용할 ReorderDifference를 받고, 프레임워크가 드래그 제스처, 들어올리기, 그리고 드롭 위치를 표시하는 플레이스홀더를 처리합니다.23 하나의 컨테이너가 여러 컬렉션을 담을 때는 컬렉션 식별자 타입을 가진 reorderContainer(for:in:isEnabled:move:) 오버로드를 사용하세요.27
draggable(containerItemID:)와 기존 draggable의 차이는 무엇인가요?
draggable(containerItemID:containerNamespace:)는 페이로드가 아니라 항목의 식별자만 전달하므로, dragContainer(for:itemID:in:_:) 안에서 지연 방식으로 동작합니다. 프레임워크는 드래그가 시작될 때만 실제로 드래그된 항목을 요청하고, 페이로드를 읽기 위해 뷰를 렌더링할 필요가 없습니다.45 그래서 모든 페이로드를 미리 만드는 것이 낭비가 될 크거나 지연 로드되는 컬렉션에 올바른 선택입니다.
새 SwiftUI 문서 모델은 FileDocument와 어떻게 다른가요?
iOS 27은 읽기와 쓰기를 별도의 프로토콜인 ReadableDocument와 WritableDocument로 분리하고, 디스크 작업은 DocumentReader/DocumentWriter가 담당하며, 읽기와 쓰기가 모두 가능한 타입을 위한 Document 타입 별칭(typealias)을 제공합니다.67 커스텀 로직이 필요 없는 작거나 중간 크기의 문서에는 FileWrapperDocumentReader와 FileWrapperDocumentWriter가 구현을 제공하고, URLDocumentConfiguration이 열린 문서를 기술합니다.101112 이 분리 덕분에 뷰어 전용 기능은 읽기 측면만 준수할 수 있습니다.
이제 SwiftUI에서 Error로부터 직접 알림을 표시할 수 있나요?
네. alert(error:actions:message:)와 alert(error:actions:)는 E : Error인 Binding<E?>를 받습니다. 바인딩된 오류가 nil이 아닐 때 시스템이 알림을 표시하고, 오류가 LocalizedError를 준수하면 제목이 그 errorDescription에서 추론되며, 그렇지 않으면 localized description을 사용합니다.1619 더 이상 오류를 커스텀 identifiable 구조체로 감싸지 않습니다.
공간이 부족할 때 어떤 툴바 항목이 사라질지 어떻게 제어하나요?
ToolbarContent의 visibilityPriority(_:)로 항목 순위를 매기세요. 공간이 줄어들면 우선순위가 낮은 항목이 우선순위가 높은 것보다 먼저 오버플로 메뉴로 이동합니다.15 topBarPinnedTrailing을 사용해 컨트롤을 후행 가장자리에 고정하면 검색이 활성화되고 공간이 없을 때만 오버플로로 이동하며, ToolbarOverflowMenu를 사용해 항상 오버플로 메뉴에 머무는 액션을 선언하세요.1314
iOS 27에서 AsyncImage가 커스텀 헤더를 보내거나 캐시 정책을 설정할 수 있나요?
네. AsyncImage(request:scale:) 계열은 헤더, 캐시 정책, 타임아웃 간격을 담는 URLRequest를 받습니다. Apple은 요청을 통해 캐시 정책과 타임아웃을 지정할 수 있다고 설명합니다.2032 하위 트리의 AsyncImage 인스턴스 전반에 걸쳐 구성된 URLSession(인증이나 커스텀 캐시를 위해)을 공유하려면 asyncImageURLSession(_:)을 적용하세요.34
전체 Apple Ecosystem 클러스터입니다. SwiftUI 기반(result builder, 불투명 타입, 값 타입 뷰 트리), iOS 27 툴바 최소화 동작이 맞물리는 Liquid Glass 패턴, 이 글의 모든 뷰 아래에서 상태 계층을 구동하는 @Observable 내부 구조, 그리고 백그라운드 실행, 동기화, Spotlight를 위한 병렬 표면인 iOS 27의 App Intents입니다. 허브는 Apple Ecosystem 시리즈에 있습니다. AI 에이전트와 함께하는 더 넓은 iOS 맥락은 iOS Agent Development 가이드를 참고하세요.
참고 자료
-
Apple Developer Documentation: SwiftUI. The framework reference covering views, lists, documents, toolbars, and the iOS 27 additions described here. ↩
-
Apple Developer Documentation:
reorderContainer(for:isEnabled:move:)(iOS 27.0 beta). Defines a container that allows its items to be reordered; the single-collection convenience that delivers aReorderDifferenceto itsmoveclosure. ↩↩↩↩↩↩ -
Apple Developer Documentation:
reorderable()(iOS 27.0 beta). Enables the views ofDynamicViewContentto be reordered when used within the scope of a reorder container. ↩↩↩↩↩ -
Apple Developer Documentation:
dragContainer(for:itemID:in:_:)(iOS 27.0 beta; macOS 26.0). A container with draggable views; takes aKeyPathto each item’s identifier and a payload closure over the dragged identifiers. ↩↩↩↩↩↩ -
Apple Developer Documentation:
draggable(containerItemID:containerNamespace:)(iOS 27.0 beta; macOS 26.0). Activates a view as a drag source inside a drag container, supplying only an identifier so the container works lazily. ↩↩↩↩↩↩ -
Apple Developer Documentation:
ReadableDocument(iOS 27.0 beta). “A type that you use to read documents from file.” Declared asprotocol ReadableDocument : AnyObject; for read-write, also conform toWritableDocumentor use theDocumenttypealias. ↩↩↩↩↩ -
Apple Developer Documentation:
WritableDocument(iOS 27.0 beta). “A type that you use to write documents to file.” Declared asprotocol WritableDocument : AnyObject; conform alongsideReadableDocumentto support saving. ↩↩↩↩↩ -
Apple Developer Documentation:
DocumentReader(iOS 27.0 beta). “Implements logic of reading documents from disk.” Declared asprotocol DocumentReader<Snapshot>. ↩↩ -
Apple Developer Documentation:
DocumentWriter(iOS 27.0 beta). “Implements logic of writing documents to disk.” Declared asprotocol DocumentWriter<Snapshot>. ↩↩ -
Apple Developer Documentation:
FileWrapperDocumentReader(iOS 27.0 beta). A document reader backed by a file wrapper; efficient for documents of small and medium size that need no custom reading logic. ↩↩↩↩ -
Apple Developer Documentation:
FileWrapperDocumentWriter(iOS 27.0 beta). A document writer backed by a file wrapper; efficient for documents of small and medium size that need no custom writing logic. ↩↩↩↩ -
Apple Developer Documentation:
URLDocumentConfiguration(iOS 27.0 beta). “A set of settings and properties of an open document.” Declared as@MainActor final class URLDocumentConfiguration. ↩↩↩ -
Apple Developer Documentation:
ToolbarOverflowMenu(iOS 27.0 beta). “The overflow menu of a toolbar.” Declared asnonisolated struct ToolbarOverflowMenu<Content> where Content : View; on iOS and visionOS the content is placed in the navigation bar’s overflow menu. ↩↩↩↩ -
Apple Developer Documentation:
topBarPinnedTrailing(iOS 27.0 beta). “A placement that pins the item to the trailing edge of the toolbar.” Pinned items only move to the overflow menu when search is active and there isn’t enough room. ↩↩↩↩↩ -
Apple Developer Documentation:
visibilityPriority(_:)(iOS 27.0 beta). “Defines the visibility priority for a toolbar item.” When toolbar space is limited, lower-priority items move into the overflow menu before higher-priority items. ↩↩↩↩↩ -
Apple Developer Documentation:
alert(error:actions:message:)(iOS 27.0 beta). “Presents an alert with a message when an error is present.” The title is inferred from the error’serrorDescriptionif it is aLocalizedError; otherwise from the localized description. ↩↩↩↩↩ -
Apple Developer Documentation:
alert(_:item:actions:)(iOS 27.0 beta). “Presents an alert using the given data to produce the alert’s content and a text view as a title.” For the alert to appear,datamust not benil. ↩↩ -
Apple Developer Documentation:
alert(_:item:actions:message:)(iOS 27.0 beta). The item-based alert overload with a message builder; the data must be non-nil and changes after presentation are ignored. ↩↩↩ -
Apple Developer Documentation:
alert(error:actions:)(iOS 27.0 beta). “Presents an alert when an error is present.” The error-binding overload without a message builder. ↩↩↩ -
Apple Developer Documentation:
init(request:scale:)(iOS 27.0 beta). “Loads and displays an image from the specified URL load request.” Declared asinit(request: URLRequest, scale: CGFloat = 1) where Content == Image; shows a placeholder until the load completes. ↩↩↩↩ -
Apple Developer Documentation:
swipeActions(edge:allowsFullSwipe:content:onPresentationChanged:)(iOS 27.0 beta). The closure is called withtruewhen a row’s swipe actions become visible andfalsewhen they are dismissed. ↩↩ -
Apple Developer Documentation:
swipeActionsContainer()(iOS 27.0 beta). Coordinates swipe-action dismissal and mutual exclusion across rows in aScrollViewor similar container; applying it to aListis a no-op. ↩↩ -
Apple Developer Documentation:
crossFade(iOS 27.0 beta). “A navigation transition that cross-fades between the appearing view and the disappearing view.” Specified on a sheet, it fades in over the content rather than moving upward to cover it. ↩↩ -
Apple Developer Documentation:
prominent(iOS 27.0 beta). “The prominent role.” Provides prominent visual treatment to one tab in supported tab bars; with no explicit.prominenttab, a.searchrole tab may receive it by default. ↩↩ -
Apple Developer Documentation:
UIHostingSceneDelegate(iOS 26.0; tvOS 27.0 beta). “ExtendsUISceneDelegateto bridge SwiftUI scenes.” Declare SwiftUI scenes to activate from UIKit in the staticrootSceneproperty of the conforming class. ↩↩↩ -
Apple Developer Documentation:
GestureInputKinds(iOS 27.0 beta). “An option set that specifies which input kinds a gesture should recognize.” ↩↩ -
Apple Developer Documentation:
reorderContainer(for:in:isEnabled:move:)(iOS 27.0 beta). “Defines a container that allows its items to be reordered.” The multi-collection overload, keyed by a collection identifier type; use it when a container holds more than one collection. ↩↩↩ -
Apple Developer Documentation:
fileExporter(isPresented:document:contentType:defaultFilename:onCompletion:onCancellation:)(iOS 27.0 beta). Presents a system dialog to export aWritableDocumentwhose writer’s destination isURL; the dialog appears only whendocumentis non-nil. ↩↩ -
Apple Developer Documentation:
toolbarMinimizeBehavior(_:for:)(iOS 27.0 beta). “Sets the minimize behavior for the specified bars.” Enables toolbar minimization in response to scrolling; the supported placement is the navigation bar, and an integrated top tab bar minimizes with it. ↩↩ -
Apple Developer Documentation:
confirmationDialog(_:item:titleVisibility:actions:message:)(iOS 27.0 beta). Presents a confirmation dialog with a message using data to produce the dialog’s content and a text view for the message. ↩ -
Apple Developer Documentation:
confirmationDialog(_:item:titleVisibility:actions:)(iOS 27.0 beta). The item-based confirmation dialog without a message builder. ↩ -
Apple Developer Documentation:
init(request:scale:transaction:content:)(iOS 27.0 beta). “Loads and displays a modifiable image from the specified URL load request in phases.” You can specify the cache policy and timeout interval via the request. ↩↩ -
Apple Developer Documentation:
init(request:scale:content:placeholder:)(iOS 27.0 beta). “Loads and displays a modifiable image from the specified URL load request using a custom placeholder until the image loads.” ↩ -
Apple Developer Documentation:
asyncImageURLSession(_:)(iOS 27.0 beta). “A modifier that adds a URL session for asynchronous images contained in the view to use when fetching image data.” ↩↩ -
Apple, WWDC26 session 269, “What’s new in SwiftUI.” developer.apple.com/videos/play/wwdc2026/269. The session frames the release around a refined look and feel, a new document API, new ways to interact, and performance improvements. ↩
-
Apple, WWDC26 session 271, “Code-along: Build powerful drag and drop in SwiftUI.” developer.apple.com/videos/play/wwdc2026/271. The reorder code is shown moving unchanged between a
Listand aLazyVGrid, since the reorderable API works with any container that supports drag and drop. ↩ -
Apple, WWDC26 session 269, “What’s new in SwiftUI.” developer.apple.com/videos/play/wwdc2026/269. Source for the “unable to type-check this expression in reasonable time” error, the
Section/Group/ForEachoverload decision tree, the unification of the common builders underContentBuilder, the step toward unified builders across SwiftUI, any-minimum-deployment-target support as an evolution ofViewBuilder, and the Xcode 27 type-checking improvement across the 2027 and previous releases. ↩↩↩↩↩↩ -
Apple Developer Documentation:
ContentBuilder. “A custom parameter attribute that constructs views and other content types from closures.” Declared as a typealias, with availability listed back to iOS 13.0 and macOS 10.15. ↩ -
Apple, WWDC26 session 8006, “SwiftUI Group Lab.” developer.apple.com/videos/play/wwdc2026/8006. Paraphrased from a locally transcribed recording of the WWDC 2026 SwiftUI Group Lab; Apple publishes no official captions for the labs. Source for the per-type builder overloads having capped the team’s own API surface (each addition worsened type-checking) and for the unified builder being usable outside views to enable SwiftUI-like custom DSLs. ↩↩
-
Apple, WWDC26 session 400, “Dub Dub Daily: Day 5”, official transcript. Holly Borla, Swift engineering manager, in the closing interview with Jeff; source for the “fallback in the compiler’s type checker” characterization, the focus on nested closures and SwiftUI view bodies, and the ongoing Open Source Swift work. ↩↩