SwiftUI 3.0의 새로운 기능! [ iOS 15, Xcode 13 ]
SwiftUI
는 Apple의 선언적 UI 프레임 워크입니다. 이번 WWDC 2021에서는 흥미로운 새로운 개선 사항, 일부 기능의 사용 중단 발표가 있었고, 개발자들에게 다시 한번 환영을 받았습니다.
SwiftUI 3.0
은 iOS 15, iPadOS 15, macOS 12 및 watchOS 12에서 사용할 수 있습니다.
시작하기 전에 주목할 사항이 있습니다. Info.plist
파일은 기본적으로 Xcode 13
Project 구조에서 더 이상 표시 되지 않습니다. 대신 프로젝트 내비게이터 탭에서 액세스 해야 합니다. 다음 몇 섹션에서는 iOS 15 용 SwiftUI
의 새로운 기능을 살펴볼 것입니다. (대부분 다른 플랫폼에서도 일부 다른 형식으로 작동 할것입니다.)
Markdown 지원 및 새로운 AttributedString API
Markdown
은 서식 있는 텍스트를 작성하기 위한 공통 언어입니다. GitHub Readme 문서에서 많은 것을 보았을 것입니다. iOS 15부터 Apple은 Foundation
프레임 워크 및 SwiftUI
에 Markdown
지원을 제공합니다. 따라서 아래와 같이 SwiftUI Tex
t에서 Markdown
구문으로 문자열을 추가할 수 있습니다.
Text("**Connect** on [Twitter](url_here)!")
위의 문자열에서 문자 범위를 사용자 정의하려면 SwiftUI
용 새로운 AttributeString
API와 UIKit
용 향상된 NSAttributedString
을 사용할 수 있습니다.
do {
let thankYouString = try AttributedString(
markdown:"**Welcome** to [website](https://example.com)")
} catch {
print("Couldn't parse: \(error)")
}
SwiftUI
Text
에서 위의 AttributeString
을 전달하거나, 새로운 AttributeScope
및 SwiftUI
Attributes
를 사용하여 사용자 지정할 수 있습니다.
SwiftUI
에서 속성 문자열을 향상시키는 방법에 대한 Zheng의 게시글을 참고할 수 있습니다.
새로운 버튼 스타일
SwiftUI
의 Button
은 이제 훨씬 더 강력 해졌습니다. 새로운 roles와 styling modifier가 있습니다.
ButtonRole
을 사용하면 버튼의 종류를 확인할 수 있습니다.
cancel
destructive
none
some()
Publisher
마찬가지로 이제 SwiftUI
버튼 스타일을 지정할 수 있습니다. buttonStyle
View 수정자를 사용하여 BorderedButtonStyle
, BorderlessButtonStyle
, PlainButtonStyle
또는 DefaultButtonStyle
을 적용할 수 있습니다.
참고 : 이러한 스타일은 열거형 스타일로 대체할 수도 있습니다.
BorderedButtons
의 경우 capsule
, roundedRectangle
또는 automatic
중 어느 것을 원하는지를 설명하는 새로운 BorderedShape가 있습니다.
다음은 iOS 15에서 SwiftUI Button
에 적용할 수 있는 다양한 스타일 및 역할 유형에 대한 간략한 설명입니다.
VStack{
Button("Plain", role: .none, action: {})
.buttonStyle(PlainButtonStyle())
Button("Automatic", role: .none, action: {})
.buttonStyle(.automatic)
Button("Log out", role: .cancel, action: {})
.buttonStyle(BorderedButtonStyle())
.tint(.yellow)
// with controlSize
Button("Cancel", role: .cancel, action: {})
.buttonStyle(.borderless)
.controlSize(.small)
.tint(.yellow)
Button("Delete", role: .destructive, action: {})
.buttonStyle(.bordered)
.controlSize(.regular)
// with controlProminence
Button(role: .destructive, action: {}, label: {
Text("Exit").frame(maxWidth: .infinity)
})
.buttonStyle(.bordered)
.controlSize(.large)
.controlProminence(.increased)
//with BorderedShape
Button(role: .destructive, action: {}, label: {
Text("Wow shape").frame(maxWidth: .infinity)
})
.buttonStyle(BorderedButtonStyle(shape: .capsule))
.controlSize(.large)
.controlProminence(.increased)
.tint(.purple)
}
위의 네 번째 코드를 보면 borderless button
이 tint color
를 따르지 않고 automatic
스타일이 현재 OS 시스템을 따르고 있는 것을 알 수 있습니다.
group container
(위 사진에서는 VStack
를 가정함)에 스타일 수정자를 설정하면 모든 Button
컨트롤에 적용됩니다.
위 이미지에서 설명하지 않은 두 가지 속성은 controlSize
와 controlProminence
입니다.
controlSize
: 사용 가능한 몇 가지 표준 옵션에서 버튼의 크기를 선택합니다.controlProminence
: 불투명도를 정의합니다. (이미지 맨 아래 두 개의 버튼)
SwiftUI Button
컨트롤의 많은 변경 사항 외에도 Apple은 LocationButton으로 알려진 SwiftUI
용 CoreLocationUI
버튼도 공개했습니다. 표준화된 현재 위치 UI를 제공하고 위치 좌표를 빠르게 가져오는 데 도움이 됩니다.
LocationButton(.sendMyCurrentLocation) {
// Fetch location with Core Location.
}.labelStyle(.titleAndIcon)
URL에서 이미지를 로드하기 위한 SwiftUI AsyncImage
이전에는 이미지에 원격 URL을 표시하려면 로더를 설정하고 asynchronous(비동기) 작업을 수행해야 했습니다. 다양한 로딩 상태를 다루는 것은 계속 증가하는 상용구 코드에만 추가되었습니다.
고맙게도 SwiftUI 3.0
은 AsyncImage
를 사용하여 전체 프로세스를 추상화합니다.
작동하는 방법은 :
AsyncImage(url: URL(String: <some_url>)!)
Xcode에서 사용해보겠습니다.
import SwiftUI
struct AsyncImageView: View {
let str = "https://unsplash.com/photos/random"
var body: some View {
VStack {
AsyncImageView(url: URL(string: str)!, content: { image in
image}, placeholder: {
Image(systemName: "pencil.and.outline")
.renderingMode(.original)
.font(.system(size: 50))
})
}
}
}
content block
의 AsyncImagePhase
열거 형을 사용하여 결과의 다양한 상태를 처리할 수도 있습니다.
AsyncImage(url: URL(string: str)!, scale: 1.0){
content in
switch content{
case .empty:
case .success(let image):
case .failure(let error):
}
AsyncImage
를 사용하면 애니메이션을 전달할 수 있는 transcation
을 지정할 수 있습니다.
크기를 제어할 수 있을 뿐만 아니라 확대 및 축소에도 편리한 또 다른 scale
매개 변수가 있습니다.
AsyncImage
는 URLSession
을 사용하기 때문에 캐싱에 대한 기본 지원을 제공합니다 (현재는 사용자 지정 캐시를 작성할 수 없음).
SwiftUI TextField, 키보드 관리 향상
iOS 15에서는 현재 활성 필드를 프로그래밍 방식으로 관리하는 새로운 속성 래퍼 (@FocusState
) 가 있습니다. 이것은 개발자가 이제 특정 텍스트 필드를 강조 표시할 수 있으므로 로그인 및 등록 양식에서 매우 유용할 것입니다.
SwiftUI
에서 TextField
에 프로그래밍 방식으로 포커스를 구현하려면 focusStated
modifier에 @FocusState
를 바인딩해야 합니다. FocusState
가 동일한 뷰에 바인딩되었는지 확인하기 위해 boolean check를 수행합니다.
예제를 한번 살펴보죠.
import SwiftUI
enum CurrentField{
case field1
case field2
}
struct TextFieldFocus: View {
@State var field1 = ""
@State var field2 = ""
@FocusState var activeState: CurrentField?
var body: some View {
VStack{
TextField("Email", text: $field1)
.focused($activeState, equals: .field1)
.submitLabel(.continue)
SecureField("Password", text: $field2)
.focused($activeState, equals: .field2)
.submitLabel(.send)
Button("Goto Email") {
activeState = .field1
}
Button("Goto Password") {
activeState = .field2
}
}
.padding()
.textFieldStyle(.roundedBorder)
}
}
submitLabel
View 수정자를 확인하십시오. 이렇게 하면 여러 다른 옵션으로 keyboard return button을 설정할 수 있습니다.
현재 SwiftUI
TextField FocusState
는 첫 번째 iOS 15 beta에서 의도 한대로 Form
내에서는 작동하지 않습니다.
새로운 onSubmit view modifier / onCommit 지원 중단
TextField
에 대한 focus management 가 향상되었기 때문에, 결과를 처리하는 새로운 방법을 갖게 된 것은 놀라운 일이 아닙니다. 반환 키에 대한 onCommit
콜백은 지원 중단되었으며, 새로운 onSubmit
view modifier가 있습니다. 이 modifier는 view 계층 구조에서 텍스트 필드 결과를 처리하기 위한 것입니다.
예제를 한번 살펴보죠.
Form{
TextField("Email", text: $field1)
SecureField("Password", text: $field2)
TextField("Name", text: $field3)
.submitScope(false) //true로 설정하면 값이 제출되지 않습니다.
}
.onSubmit {
print(field1)
print(field2)
print(field3)
}
참고
선택적으로 .onSubmit(of :)
를 사용하여 view type을 지정할 수 있습니다.
또한 submitScope
를 true
로 설정하면 해당 계층의 결과가 리턴되지 않습니다.
SwiftUI List 검색 기능 추가
SwiftUI 1.0
과 2.0
의 List
에서는 검색 기능이 없었습니다. iOS 15에서 Apple은 List
에 searchable
수정자를 제공합니다. 기억해야 할 핵심 사항은 List
를 searchable
으로 표시하려면 NavigationView
안에 래핑 해야 합니다.
search view가 있는 SwiftUI List
를 먼저 살펴보겠습니다.
struct Colors : Identifiable{
var id = UUID()
var name : String
}
struct SearchingLists: View {
@State private var searchQuery: String = ""
@State private var colors: [Colors] = [Colors(name: "Blue"),
Colors(name: "Red"),
Colors(name: "Green"),
Colors(name: "Yellow"),
Colors(name: "Pink"),
Colors(name: "Purple"),
Colors(name: "White"),]
var body: some View {
NavigationView {
List($colors) { $color in
Text(color.name)
}
.searchable("Search color", text: $searchQuery, placement: .automatic){
Text("re").searchCompletion("red")
Text("b")
}
.navigationTitle("Colors List")
}
}
}
고려해야 할 몇 가지 사항이 있습니다.
searchable
을 사용하면 여러 검색 제안을 지정할 수 있습니다. 탭 하면 검색 창에searchCompletion
텍스트가 표시됩니다.- Automatic
placement
는 Apple 플랫폼을 기반으로 검색 창의 위치를 결정합니다. - 이제
List
에서 직접Binding
배열을 전달하고 각 행에 대한Binding
값을 가져올 수 있습니다.SwiftUI TextField
에 바인딩 값이 필요합니다. 이전에는List
에서TextField
를 설정하면 전체 뷰 계층 구조가 다시 로드되는 경우가 많았습니다. 고맙게도List
의Binding
배열 지원은 이러한 문제를 없애줍니다.
검색 결과를 표시하려면 위에서 설명한 onSubmit
수정자를 사용하거나 아래와 같이 실시간으로 필터링하는 계산된 속성을 설정할 수 있습니다.
import SwiftUI
struct Colors : Identifiable{
var id = UUID()
var name : String
}
struct SearchingLists: View {
@State private var searchQuery: String = ""
@State private var colors: [Colors] = [Colors(name: "blue"),
Colors(name: "Red"),
Colors(name: "Green"),
Colors(name: "Yellow"),
Colors(name: "Pink"),
Colors(name: "Purple"),
Colors(name: "White"),
Colors(name: "Orange"),
Colors(name: "Black"),]
var body: some View {
NavigationView {
List {
ForEach (searchResults, id:\.id){ color in
Text(color.name)
}
}
.searchable("Search color", text: $searchQuery,
placement: .automatic)
.navigationTitle("Colors List")
}
}
var searchResults: [Colors] {
if searchQuery.isEmpty{
return colors
}
else{
return colors.filter {$0.name.lowercased().contains(searchQuery.lowercased())}
}
}
}
사용자가 search view와 상호 작용하는지 추적하기 위한 환경 값 isSearching
도 있습니다. overlay 또는 다른 보기에서 검색 결과를 표시하거나 숨기는 데 사용할 수 있습니다.
중첩된 SwiftUI List
와 함께 onSubmit
수정자를 사용하는 것이 아주 흥미로울 것입니다.
SwiftUI List Refresh
위에서 아래로 당겨서 새로고침 하는 기능도 이전에는 존재하지 않았습니다. 때문에 UIViewRepresentable
이라는 해결 방법을 사용해야 했습니다. 하지만 iOS 15에서는 아래와 같이 refreshable
수정자를 사용하여 몇 줄의 코드로 통합할 수 있습니다.
var body: some View {
NavigationView {
List($colors){ $color in
Text(color.name)
}
.refreshable {
self.colors.append(Colors(name: "Newly added"))
}
.navigationTitle("Colors List")
}
}
refreshable
수정자 내에서 async
/ await
와 같은 네트워크 요청을 가져오고 결과를 표시할 수 있습니다.
현재 기본 SwiftUI Pull to Refresh는 SwiftUI
Gird 또는 Scroll View
에서 작동하지 않습니다. 그러나 어느 정도 사용자 지정이 가능합니다. 새로 고침을 수동으로 트리거하는 RefreshAction
환경 값이 있습니다.
SwiftUI Lists Swipe Actions
Swipe하여 작업을 수행하기 위한 또 다른 기능이 요청되었었고, 올해 새로운 swipeActions
수정자가 추가되었습니다.
이번에 추가된 새로운 button style을 사용한 swipe작업을 살펴보겠습니다.
List {
ForEach (colors, id:\.id){ color in
Text(color.name)
.swipeActions{
Button(role: .destructive){
//do something
}label: {
Label("Delete", systemImage: "xmark.bin")
}
Button{
//do something
}label: {
Label("Pin", systemImage: "pin")
}
}
}
}
기본적으로 swipe동작은 화면의 뒤쪽 가장자리에서 표시됩니다. 하지만 앞쪽에서 작동하도록 구성할 수도 있습니다. 사용방법은 아래와 같습니다.
.swipeActions(edge: .leading)
여러 수정자를 연결하여 양쪽에서 swipe동작을 지원할 수도 있습니다.
기본적으로 첫 번째 swipe동작은 전체 swipe에서 트리거 됩니다. 그러나 allowFullSwipe
를 false
로 설정하여 변경할 수 있습니다.
현재 swipe동작은 Array List
바인딩에서 작동하지 않습니다.
Lists, Tabs, Alerts, and Sheets에 대한 추가 Modifiers
SwiftUI List
는 의심할 여지없이 올해 가장 큰 업그레이드를 받았습니다. search view
, refresh
, siwpe
작업만으로도 충분하지 않다면 List를 사용자화 할 수 있는 다양한 방법이 있습니다.
- 구분선을 표시하거나 숨기는
listRowSeparator
및listSectionSeparator
. listRowSeparatorTint
및listSectionSeparatorTint
를 사용하여 각 행과 섹션에 각각 색조를 추가합니다.- macOS의 경우
.insetStyle (alternatesRowBackgrounds:)
사용. badge
는 SwiftUI List row 의 오른쪽에 설정됩니다. iOS 15의 SwiftUITabViews
에서도 사용할 수 있습니다.
SwiftUI Alert view
는 이제 더 이상 사용되지 않습니다. 대신 새로운 .alert
수정자가 있습니다. .alert
는 또한 error dialogs에 대한 변형을 제공합니다.
form
및 sheet
를 바로 닫을 수 없도록 개발자가 선택할 수 있는 interactiveDismissDisabled
함수가 있습니다. 간단한 예는 아래와 같습니다.
import SwiftUI
struct PressentingView: View {
@State var showTerms: Bool = false
var body: some View {
Button("Show Sheet") {
showTerms = true
}
.sheet(isPresented: $showTerms) {
Sheet()
}
}
}
struct Sheet: View {
@State private var acceptedTherms = false
var body: some View {
Form {
Button("Accept Terms") {
acceptedTherms = true
}
}
.interactiveDismissDisabled(!acceptedTherms)
}
}
동시성을 위한 Task Modifier
이전에는 View
에 대한 데이터를 가져올 때 많은 SwiftUI
개발자가 onAppear
에 의존했지만, iOS 15에서 Apple은 데이터를 비동기 적으로 로드할 수 있는 완전히 새로운 Task
수정자를 제공합니다.
첨부된 SwiftUI View
가 삭제될 때 자동으로 작업을 취소하는 기능은 Task
수정자를 매우 편리한 도구로 만들어줍니다.
요청 시 더 많은 데이터를 로드하여 끝없는 스크롤 목록을 만드는 것은 Task
수정자의 완벽한 사용방법(사례)입니다. 또한 NavigationLink
destination views(아쉽게도 이부분은 개선되지 않았습니다😭)를 무거운 작업을 수행하는 데 유용합니다.
새로운 SwiftUI Material Struct
Material struct
는 뷰에 반투명과 생동감을 추가하여 전경 요소를 background
와 혼합하는 데 사용됩니다. 다음과 같은 방식으로 사용합니다.
.background(.regularMaterial)
.background(.thinMaterial)
.background(.ultraThinMaterial)
.background(.thickMaterial)
.background(.ultraThicknMaterial)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
기본적으로 material type은 background type이 전경 요소를 통과하는 정도를 나타냅니다. SwiftUI view
를 시각적으로 흐리게 하는 좋은 대안입니다.
새로운 Canvas 및 타임 라인 View
SwiftUI
의 drawRect
는 2 년 동안 사라졌었습니다. 마지막으로 Apple은 이를 Canvas API 등의 형태로 제공하고 있습니다.
Canvas API는 drawing paths
및 shapes
대한 기본 지원을 제공합니다. 따라서 SwiftUI
앱에서 mask를 설정하고 필터를 변환 및 적용하여 풍부한 그래픽을 만들 수 있습니다. 제일 좋은 부분은 바로 CPU 가속입니다.
반면 TimelineView
는 실시간으로 업데이트되는 동적 보기를 구성하는 기능을 도입합니다. 우리는 작년에 SwiftUI Text
에서 Timer
게시자를 엿볼 수 있었습니다.
그러나 TimelineView
는 SwiftUI view
를 래핑하고 TimelineSchedule
을 통해 특정 시간에 정기적 또는 구성할 수 있는 예약을 보고 설정합니다. TimelineView
는 앱 내부의 위젯과 같습니다.
WWDC 2021에서 Apple은 SwiftUI
를 사용하여 구축된 다양한 기본 iOS 앱을 선보였습니다. TimelineViews
가 이들 중 일부에서 큰 역할을 했다고 확신합니다.
디버깅
SwiftUI
는 이제 뷰 계층 구조 디버깅을 위한 내장된 지원을 제공합니다.
View 본문 내에서 다음 함수를 호출하면 마지막 다시 로드 시 수정된 SwiftUI view
가 인쇄됩니다.
let _ = Self._printChanges()
결론
이 게시글에서는 WWDC 2021에서 발표된 주요 SwiftUI
기능 중 일부를 살펴보았습니다. 그러나 macOS
의 SwiftUI Table
사용이나 SwiftUI
앱에 더 많은 동시성을 추가하기 위한 CoreData
의 SectionedFetchRequest
속성 래퍼 사용과 같이 여전히 기대할 것이 많습니다.
읽어주셔서 감사합니다🤟
SwiftUI 3.0 관련 Youtube 영상
- Kavsoft : What's New in SwiftUI for iOS 15 - WWDC 2021 - Xcode 13
- Paul Hudoson : What's new in SwiftUI for iOS 15 part 1
- Paul Hudoson : What's new in SwiftUI for iOS 15 part 2