SwiftUI Study - kirseia/study GitHub Wiki
SwiftUI 공부 (with hackingwithswift.com)
-
Ref. https://www.hackingwithswift.com/quick-start/swiftui/swiftui-tips-and-tricks
-
준비물
- osx 10.15 beta + xcode 11 beta
- osx 10.15 beta 가 없으면 SwiftUI 의 Preview가 동작하지 않습니다
만들어보기
- TabbedView를 포함하기
- 첫 번째 탭은 List 를 포함하고, Detail View를 가지고 있음. - List <-> Detail view 사이에 @ObjectBinding 로 데이터 전달
- 두 번째 탭은 Setting, Setting에 있는 어떤 값을 @EnvironmentObject 로 저장해서 list/detail 에서 사용할 수 있게 하기
UIKit -> SwiftUI 만들기
- 모든 UIKit을 SwiftUI로 대체할 순 없음
- UICollectionView / UITextView 는 대체 불가
목표
-
기본적인 UI 사용을 익혀서 뷰를 구성할 수 있다
-
애니메이션 처리 해본다
-
기본 Storyboard / xib 랑 같이 사용 할 수 있나 알아본다
- SwiftUI 에서 XIB / Storyboard의 view를 사용 할 수 있다.
- storyboard 사용 가능함
- viewController 로 사용은 안되고, viewController의 view를 이용할 수 있음
- xib 사용 가능함
- UIViewRepresentable protocol 을 활용하면 됨
- Ref. https://wwdcbysundell.com/2019/swiftui-common-questions/
- Ref. https://qiita.com/kntkymt/items/dcb0a1bf70a83ba6b3c9
-
ViewController 처리
-
UIViewControllerRepresentable
* 기본 뷰 만들기
struct 뷰_이름: View {
var body: some View {
VStack(.leading) {
Text("Hello")
Text("World")
}
}
}
Text(), Rectangle(), Circle(), HStack(), VStack(), ZStack() 등등 배경색, 채움색, padding, scale 등등 다양한 옵션 선택 가능
- 프리뷰는 Option + Cmd + P 로 바로 reload & resume 가능
* 타입이 정해져있지 않은 뷰
// https://www.hackingwithswift.com/quick-start/swiftui/how-to-return-different-view-types
var body: some View {
Group {
if Bool.random() {
Image("example-image")
} else {
Text("Better luck next time")
}
}
}
와 같이 Group 을 묶어서 특정 뷰 타입을 return 하지 않을 때 처리 할 수 있음. AnyView를 써도 되지만 좀 더 복잡함.
* Loop 사용하기
- ForEach() 를 사용하면 됨
struct ContentView : View {
let colors: [Color] = [.red, .green, .blue]
var body: some View {
VStack {
ForEach(colors.identified(by: \.self)) { color in
Text(color.description.capitalized)
.padding()
.background(color)
}
}
}
}
특정 타입의 값을 가져오기위해서는 identified(by:)를 사용해야 함. 뷰를 찾기 위해서 필요한 값
* Environment 가져오기 (동작 안하는 듯??)
struct ContentView : View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
var body: some View {
if horizontalSizeClass == .compact {
return Text("Compact")
} else {
return Text("Regular")
}
}
}
- 왜 인지 동작 안함.
* 뷰를 safe area 넘어에 위치 시키기
- 기본적으로 SwiftUI 는 safeArea 안에 놓이게 되어있음.
Text("Hello World")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
// 프리뷰상에서 safe area를 넘지 않음. 왜 동작 안하는지 모르겠음 -_-)
- 기기에서 돌려봐야 알 듯
// https://www.hackingwithswift.com/quick-start/swiftui/how-to-place-content-outside-the-safe-area
* Preview 여러 장비 세팅하기
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.previewDevice(PreviewDevice(rawValue: "iPhone SE"))
.previewDisplayName("iPhone SE")
ContentView()
.previewDevice(PreviewDevice(rawValue: "iPhone XS Max"))
.previewDisplayName("iPhone XS Max")
}
}
}
#endif
// rawValue를 쓰는게 불편. 아마도 정식에서는 enum 같은걸로 바로 붙일 수 있지 않을라나
// https://www.hackingwithswift.com/quick-start/swiftui/how-to-preview-your-layout-in-different-devices
* 상태 이용하기
-
선언형 vs 명령형 프로그래밍
-
Reactive 스타일 = 선언형 프로그래밍 스타일
-
명령형은 하나하나 열거하는데 반해 , 선언형은 결국 뭘 하고 싶은지를 정해놓는다고 생각하면 될 듯.
-
예를 들어 식당에 가서 밥을 먹으려고 하면...
-
명령형은 식당에 간다 -> 둘러본다 -> 자리를 확인한다 -> 인원수에 맞는지 체크한다 -> 자리에 가서 앉는다.
-
선언형은 식당에서 인원수에 해당하는 인원이 앉을 수 있는지 물어본다.
-
(-_- 설명이 뭔가 이상하지만...)
-
자세한건 Reactive 쪽을 살펴보는게...
-
-
암튼 Binding 을 이제 이용할 수 있게 되었음. @State 로 변수 만들고, 각 UI에서 binding 지원하는 곳에서 $변수 로 세팅 가능, 사용은 그냥 변수 이름으로 사용 할 수 있음
struct ContentView : View {
@State var showGreeting = true
var body: some View {
VStack {
Toggle(isOn: $showGreeting) {
Text("Show welcome message")
}.padding()
if showGreeting {
Text("Hello World!")
}
}
}
}
@State 를 이용해서 상태값을 활용할 수 있게 됨
- Toggle 안에는 $showGreeting 인데 if 에는 showGreeting이 바로 들어감. 무슨 차이일까?
Picker 사용
struct ContentView : View {
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}
@State var birthDate = Date()
var body: some View {
VStack {
DatePicker(
$birthDate,
maximumDate: Date(),
displayedComponents: .date
)
Text("Date is \(birthDate, formatter: dateFormatter)")
}
}
}
Gesture 달기
Image("example-image")
.tapAction(count: 2) {
print("Double tapped!")
}
struct ContentView : View {
@State private var scale: Length = 1.0
var body: some View {
Image("example-image")
.scaleEffect(scale)
.gesture(
TapGesture()
.onEnded { _ in
self.scale += 0.1
}
)
}
}
뷰 생명 주기 이벤트 받기 (appear / disappear)
struct ContentView : View {
var body: some View {
NavigationView {
NavigationButton(destination: DetailView()) {
Text("Hello World")
}
}.onAppear {
print("ContentView appeared!")
}.onDisappear {
print("ContentView disappeared!")
}
}
}
struct DetailView : View {
var body: some View {
VStack {
Text("Second View")
}.onAppear {
print("DetailView appeared!")
}.onDisappear {
print("DetailView disappeared!")
}
}
}
.onAppear { } .onDisappear { }
활용해서 사용 가능
-
근데 실제 실행해보면 print 가 실행이 안됨. debug도 안됨. 원래 안되는지 확인 필요
- https://www.hackingwithswift.com/quick-start/swiftui/swiftui-tips-and-tricks
- How to make print() work 참고, play 버튼에 우클릭해서 debug preview 하면 됨
-
onDisappear() 는 호출이 안됨 - xcode 11 beta 1 에서는 안되는 듯
@ObjectBinding, @State, @EnvironmentObject 차이?
- @State : 일반 적인 상태 저장용
- @ObjectBinding : 커스텀 타입이나 여러 뷰에 걸쳐서 공유되는 값들에 사용
- @ObjectBinding을 사용하려면 BindableObject protocol 을 구현해야 함
- BindableObject 는 didChange 하나만 구현하면 되긴 함. - 반드시 main thread에서 해야 함
- 가장 유용함
- @EnvironmentObject
- 어플리케이션에서 사용되는 값, 모든 뷰에 공유 됨. static 변수 같은...
@ObjectBinding 사용하기
class UserSettings: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var score = 0 {
didSet {
didChange.send(())
}
}
}
struct ContentView : View {
@ObjectBinding var settings = UserSettings() // <- not private !!!
var body: some View {
VStack {
Text("Your score is \(settings.score)")
Button(action: {
self.settings.score += 1
}) {
Text("Increase Score")
}
}
}
}
@ObjectBinding 은 private 이 아님. 왜냐하면 다른 뷰에서 써야 하니까 (보통 shared 하기 위해서 만든거니까...)
@ObjectBinding 다른 뷰랑 공유하기
@EnvironmentObject 사용해보기
SceneDelegate.swift 에 var settings = UserSettings() 를 추가한다.
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIHostingController(rootView: ContentView())
->
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(settings)
struct ContentView : View {
@EnvironmentObject var settings: UserSettings
var body: some View {
NavigationView {
VStack {
// A button that writes to the environment settings
Button(action: {
self.settings.score += 1
}) {
Text("Increase Score")
}
NavigationButton(destination: DetailView()) {
Text("Show Detail View")
}
}
}
}
}
struct DetailView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
// A text view that reads from the environment settings
Text("Score: \(settings.score)")
}
}
하면 된다는데 왜 빌드가 안될까...
List - UITableView 와 비슷한 것
struct RestaurantRow: View {
var name: String
var body: some View {
Text("Restaurant: \(name)")
}
}
struct ContentView: View {
var body: some View {
List {
RestaurantRow(name: "Joe's Original")
RestaurantRow(name: "The Real Joe's Original")
RestaurantRow(name: "Original Joe's")
}
}
}
-
List 에 다른 타입이 들어가도 됨
-
여러 타입을 사용하려면 각 아이템을 identify 할 수 있어야 함 -> Identifiable protocol을 구현해주면 됨
struct Restaurant: Identifiable {
var id = UUID()
var name: String
}
struct RestaurantRow: View {
var restaurant: Restaurant
var body: some View {
Text("Come and eat at \(restaurant.name)")
}
}
struct ContentView: View {
var body: some View {
let first = Restaurant(name: "Joe's Original")
let second = Restaurant(name: "The Real Joe's Original")
let third = Restaurant(name: "Original Joe's")
let restaurants = [first, second, third]
return List(restaurants) { restaurant in
RestaurantRow(restaurant: restaurant)
}
}
}
OR
struct Restaurant: View, Identifiable {
var id = UUID()
var name: String
var body: some View {
Text("name : \(name)")
}
}
struct ListView: View {
var body: some View {
List {
Restaurant(name: "kfc")
Restaurant(name: "lotteria")
}
}
}
List 조작하기
struct ContentView : View {
@State var users = ["Paul", "Taylor", "Adele"]
var body: some View {
NavigationView {
List {
ForEach(users.identified(by: \.self)) { user in
Text(user)
}
.onDelete(perform: delete)
}
}
}
func delete(at offsets: IndexSet) {
if let first = offsets.first {
users.remove(at: first)
}
}
}
좌로 밀면 delete 가 나오고 적용 가능함
struct ContentView : View {
@State var users = ["Paul", "Taylor", "Adele"]
var body: some View {
NavigationView {
List {
ForEach(users.identified(by: \.self)) { user in
Text(user)
}
.onMove(perform: move)
}
.navigationBarItems(trailing: EditButton())
}
}
func move(from source: IndexSet, to destination: Int) {
// sort the indexes low to high
let reversedSource = source.sorted()
// then loop from the back to avoid reordering problems
for index in reversedSource.reversed() {
// for each item, remove it and insert it at the destination
users.insert(users.remove(at: index), at: destination)
}
}
}
- .navigationBarItems(trailing: EditButton()) 이거 추가 안하면 동작 안함.
- edit 버튼 누르면 삭제 가능
List에 Section 추가
struct ContentView : View {
var body: some View {
List {
Section(header: Text("Important tasks")) {
TaskRow()
TaskRow()
TaskRow()
}
Section(header: Text("Other tasks")) {
TaskRow()
TaskRow()
TaskRow()
}
}
}
}
간단. 그냥 section 추가 하면 됨. Section에는 header / footer 도 사용 가능
Section(header: Text("Other tasks"), footer: Text("End")) {
TaskRow()
TaskRow()
TaskRow()
}
struct ExampleRow: View {
var body: some View {
Text("Example Row")
}
}
struct ContentView : View {
var body: some View {
List {
Section(header: Text("Examples")) {
ExampleRow()
ExampleRow()
ExampleRow()
}
}.listStyle(.grouped)
}
}
- Group 스타일 만들기
List 아이템은 묵시적으로 HStack으로 감싸져 있음
struct ContentView : View {
let users = [User(), User(), User()]
var body: some View {
List(users) { user in
Image("paul-hudson")
.resizable()
.frame(width: 40, height: 40)
Text(user.username)
}
}
}
-
Image 와 Text는 HStack으로 감싸진것처럼 옆으로 나란히 나온다.
-
리스트의 최소 높이를 더 줄이고 싶으면 어떻게 하지? HStack을 키우면 늘어나는데 줄인다고 더 줄어들진 않는데...
View 붙여 쓰기
struct ContentView : View {
var body: some View {
Text("Colored ")
.color(.red)
+
Text("SwifUI ")
.color(.green)
+
Text("Text")
.color(.blue)
}
}
이런식으로 쓰면 붙어서 나옴, HStack 으로 붙이는건 Padding 같은게 기본으로 들어가는데, 위 방식은 text attributes 만 바꿔서 넣는 것처럼 동작 함.
반복적인 Style 미리 만들어서 사용하기
- ViewModifier 라고 함
struct PrimaryLabel: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.black)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
}
}
struct ContentView : View {
var body: some View {
Text("Hello World")
.modifier(PrimaryLabel())
}
}
Form 사용하기
- SwiftUI 의 Form 은 기본적으로 container 처럼 동작 (HStack 이나 VStack 처럼)
NavigationView
var body: some View {
NavigationView {
Text("SwiftUI")
.navigationBarTitle(Text("Welcome"), displayMode: .inline) // displayMode 로 large / inline 선택 가능
.navigationBarItems(trailing:
Button(action: {
print("Help tapped!")
}) {
Text("Help")
})
}
}
SwiftUI view 갯수 제한
- 최대 10개까지만 됨
- 넘게 하려면 Group {} 을 활용하면 됨
var body: some View {
VStack {
Group {
Text("Line")
Text("Line")
Text("Line")
Text("Line")
Text("Line")
Text("Line")
}
Group {
Text("Line")
Text("Line")
Text("Line")
Text("Line")
Text("Line")
}
}
}
Alert 사용하기
struct ContentView : View {
@State var showingAlert = false
var body: some View {
Button(action: {
self.showingAlert = true
}) {
Text("Show Alert")
}
.presentation($showingAlert) {
Alert(title: Text("Are you sure you want to delete this?"), message: Text("There is no undo"), primaryButton: .destructive(Text("Delete")) {
print("Deleting...")
}, secondaryButton: .cancel())
}
}
}
네비게이션 버튼으로 새 뷰 Push 하기
struct DetailView: View {
var body: some View {
Text("Detail")
}
}
struct ContentView : View {
var body: some View {
NavigationView {
NavigationButton(destination: DetailView()) {
Text("Click")
}.navigationBarTitle(Text("Navigation"))
}
}
}
- NavigationButton(destination: DetailView()) 이게 핵심.
리스트에서 보여주기
struct RestaurantView: View {
var restaurant: Restaurant
var body: some View {
Text("Come and eat at \(restaurant.name)")
.font(.largeTitle)
}
}
struct ContentView: View {
var body: some View {
let first = Restaurant(name: "Joe's Original")
let restaurants = [first]
return NavigationView {
List(restaurants) { restaurant in
// RestaurantRow(restaurant: restaurant)
NavigationButton(destination: RestaurantView(restaurant: restaurant)) {
RestaurantRow(restaurant: restaurant)
}
}.navigationBarTitle(Text("Select a restaurant"))
}
}
}
네비게이션 Push 대신 Present VC 하기
struct DetailView: View {
var body: some View {
Text("Detail")
}
}
struct ContentView : View {
var body: some View {
PresentationButton(Text("Click to show"), destination: DetailView())
}
}
뷰에 커스텀 프레임 세팅하기
Button(action: {
print("Button tapped")
}) {
Text("Welcome")
.frame(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 200)
.font(.largeTitle)
}
OR
Text("Please log in")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.font(.largeTitle)
.foregroundColor(.white)
.background(Color.red)
VStack {
Text("Home")
Text("Options")
.offset(y: 15)
.padding(.bottom, 15) // padding 없으면 아래 뷰랑 겹쳐서 나옴
Text("Help")
}
뷰 데코레이션 순서
Text("Hacking with Swift")
.background(Color.black)
.foregroundColor(.white)
.padding()
Text("Hacking with Swift")
.padding()
.background(Color.black)
.foregroundColor(.white)
- 첫 번째는 컬러 적용 한 뒤에 확장하는거라서 패딩 영역에는 백그라운드 컬러가 들어가지 않음
- 두 번째는 패딩 적용 한 뒤에 확장하는거라서 패딩 영역에 백그라운드 컬러가 들어감
Text("Forecast: Sun")
.font(.largeTitle)
.foregroundColor(.white)
.padding()
.background(Color.red)
.padding()
.background(Color.orange)
.padding()
.background(Color.yellow)
- foregroundColor를 위 코드 처럼 넣으면 폰트 컬러가 white 가 안들어감,
- 마지막 background(Color.yellow) 뒤에 넣으면 잘 됨 (이유는?!)
기타 뷰 꾸미기
Text("Hello world")
.border(Color.red, width: 4, cornerRadius: 16)
Text("Hacking with Swift")
.padding()
.shadow(color: .red, radius: 5)
.border(Color.red, width: 4)
Text("Hacking with Swift")
.padding()
.shadow(color: .red, radius: 5, x: 20, y: 20)
.border(Color.red, width: 4)
Text("Hacking with Swift")
.padding()
.border(Color.red, width: 4)
.shadow(color: .red, radius: 5, x: 20, y: 20)
Text("Hacking with Swift")
.padding()
.border(Color.red, width: 4)
.clipped()
.shadow(color: .red, radius: 5, x: 20, y: 20)
- 첫 번째는 텍스트에 그림자, 두 번째도 텍스트에 그림자인데 x:20, y:20 위치 이동
- 세 번째는 border에 그림자가 들어감
- 네 번째는 clipped()를 하면 그 만큼만 보여줌.
모양대로 자르기
Button(action: {
print("Button tapped")
}) {
Image(systemName: "bolt.fill")
.foregroundColor(.white)
.padding()
.background(Color.green)
.clipShape(Circle())
}
Button(action: {
print("Button tapped")
}) {
Image(systemName: "bolt.fill")
.foregroundColor(.white)
.padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20))
.background(Color.green)
.clipShape(Capsule())
}
- clipShape(Capsule()) -> 이건 cornerRadius + clipToBounds 한거랑 비슷함
뷰 회전 처리
struct ContentView: View {
@State var rotation: Double = 0
var body: some View {
VStack {
Slider(value: $rotation, from: 0.0, through: 360.0, by: 1.0)
Text("Up we go")
.rotationEffect(.degrees(rotation))
// .rotationEffect(.radians(.pi))
}
}
}
struct ContentView: View {
@State var rotation: Double = 0
var body: some View {
VStack {
Slider(value: $rotation, from: 0.0, through: 360.0, by: 1.0)
Text("Up we go")
.rotationEffect(.degrees(rotation), anchor: UnitPoint(x: 0, y: 0))
}
}
}
// anchor 지정해서 center 같은걸 지정할 수 있음
뷰 변형 기타
// scale 조절
Text("Hello world").scaleEffect(5)
Text("Hello world").scaleEffect(5, anchor: UnitPoint(x: 1, y: 1))
// corner Radius
Text("Round Me")
.padding()
.background(Color.red)
.cornerRadius(25)
// opacity - 아래는 텍스트 뷰 전체에 opacity 적용됨
Text("Now you see me")
.padding()
.background(Color.red)
.opacity(0.3)
// accent color - Button text 가 accent color 가 됨
VStack {
Button(action: {}) {
Text("Tap here")
}
}.accentColor(Color.orange)
// mask 처리
Image("stripes")
.resizable()
.frame(width: 300, height: 300)
.mask(Text("SWIFT!")
.font(Font.system(size: 72).weight(.black)))
// blur 처리
Image("paul-hudson")
.resizable()
.frame(width: 300, height: 300)
.blur(radius: 20)
// blending 처리 - z-order를 이용, 아래처럼 blendMode이용해야 함
ZStack {
Image("paul-hudson")
Image("example-image")
.blendMode(.multiply)
}
// constrast / saturation, colorMuliply
Image("paul-hudson")
.contrast(0.5)
Image("paul-hudson")
.saturation(0.5)
Image("paul-hudson")
.colorMultiply(.red)
애니메이션 처리
struct ContentView: View {
@State var angle: Double = 0
@State var borderThickness: Length = 1
var body: some View {
Button(action: {
self.angle += 45
self.borderThickness += 1
}) {
Text("Tap here")
.padding()
.border(Color.red, width: borderThickness)
.rotationEffect(.degrees(angle))
.animation(.basic())
}
}
}
Text("Tap here")
.scaleEffect(scale)
.animation(.basic(duration: 3))
Text("Tap here")
.scaleEffect(scale)
.animation(.basic(curve: .easeIn))
// spring 애니메이션 !
Button(action: {
self.angle += 45
}) {
Text("Tap here")
.padding()
.rotationEffect(.degrees(angle))
.animation(.spring(mass: 1, stiffness: 1, damping: 0.1, initialVelocity: 10))
}
struct ContentView : View {
@State var showingWelcome = false
var body: some View {
VStack {
Toggle(isOn: $showingWelcome.animation()) {
Text("Toggle label")
}
if showingWelcome {
Text("Hello World")
}
}
}
}
-[ ] 원래라면 toggel 버튼 누르면 아래 text가 애니메이션되면서 나와야 하지만 버그 때문인지 동작 안함;
struct ContentView: View {
@State var opacity: Double = 1
var body: some View {
Button(action: {
withAnimation {
self.opacity -= 0.2
}
}) {
Text("Tap here")
.padding()
.opacity(opacity)
}
}
}
// withAnimation { } 을 이용해서 시간이나 애니메이션 타입등을 지정할 수 있음
애니메이션 + Transition
struct ContentView: View {
@State var showDetails = false
var body: some View {
VStack {
Button(action: {
withAnimation {
self.showDetails.toggle()
}
}) {
Text("Tap to show details")
}
if showDetails {
Text("Details go here.")
}
}
}
}
// transition 정할 수 있음
Text("Details go here.")
.transition(.move(edge: .bottom))
// or
Text("Details go here.")
.transition(.slide)
// or
Text("Details go here.")
.transition(.scale())
// combine & custom transition
Text("Details go here.").transition(AnyTransition.opacity.combined(with: .slide))
// ->
extension AnyTransition {
static var moveAndScale: AnyTransition {
AnyTransition.move(edge: .bottom).combined(with: .scale())
}
}
Text("Details go here.").transition(.moveAndScale)
// 보여질때랑 사라질때 서로 다른 애니메이션 사용하기 - .asymmetric 을 사용하면 됨
Text("Details go here.").transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .bottom)))
Custom View 합쳐서 사용하기
struct User {
var name: String
var jobTitle: String
var emailAddress: String
var profilePicture: String
}
struct ProfilePicture: View {
var imageName: String
var body: some View {
Image(imageName)
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
}
}
struct EmailAddress: View {
var address: String
var body: some View {
HStack {
Image(systemName: "envelope")
Text(address)
}
}
}
struct UserDetails: View {
var user: User
var body: some View {
VStack(alignment: .leading) {
Text(user.name)
.font(.largeTitle)
.foregroundColor(.primary)
Text(user.jobTitle)
.foregroundColor(.secondary)
EmailAddress(address: user.emailAddress)
}
}
}
struct UserView: View {
var user: User
var body: some View {
HStack {
ProfilePicture(imageName: user.profilePicture)
UserDetails(user: user)
}
}
}
struct ContentView: View {
let user = User(name: "Paul Hudson", jobTitle: "Editor, Hacking with Swift", emailAddress: "[email protected]", profilePicture: "paul-hudson")
var body: some View {
UserView(user: user)
}
}
뷰 합치기
// Text() 에 .font 하면 return type이 그대로 Text() 라서 가능
var body: some View {
Text("SwiftUI ")
.font(.largeTitle)
+ Text("is ")
.font(.headline)
+ Text("awesome")
.font(.footnote)
}
// Text() 에 .foregroundColor 하면 return type이 변경되어서 동작하지 않음
Text("SwiftUI ")
.foregroundColor(.red)
+ Text("is ")
.foregroundColor(.orange)
+ Text("awesome")
.foregroundColor(.blue)
뷰를 프로퍼티 처럼 사용하기
struct ContentView : View {
let title = Text("Paul Hudson")
.font(.largeTitle)
let subtitle = Text("Author")
.foregroundColor(.secondary)
var body: some View {
VStack {
title
subtitle
}
}
}
// 이렇게 한번 설정해놓고 추가로 .color() 처럼 변경 가능, 당연히 원본 title 을 변경하진 않음
VStack {
title
.color(.red)
subtitle
}
뷰 modifier 만들기
struct PrimaryLabel: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.red)
.foregroundColor(Color.white)
.font(.largeTitle)
}
}
struct ContentView : View {
var body: some View {
Text("Hello, SwiftUI")
.modifier(PrimaryLabel())
}
}
디버그 프리뷰 설정
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.environment(\.sizeCategory, .extraSmall)
ContentView()
ContentView()
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
}
}
}
#endif
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.environment(\.colorScheme, .light)
ContentView()
.environment(\.colorScheme, .dark)
}
}
}
#endif
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.previewDevice(PreviewDevice(rawValue: "iPhone SE"))
.previewDisplayName("iPhone SE")
ContentView()
.previewDevice(PreviewDevice(rawValue: "iPhone XS Max"))
.previewDisplayName("iPhone XS Max")
}
}
}
#endif
// NavigationView 를 debug view에서 붙여서 테스트 할 수도 있음.
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
}
}
}
#endif
SwiftUI View 프로파일링 하기
import Combine
import SwiftUI
class FrequentUpdater: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var timer: Timer?
init() {
timer = Timer.scheduledTimer(
withTimeInterval: 0.01,
repeats: true
) { _ in
self.didChange.send(())
}
}
}
struct ContentView : View {
@ObjectBinding var updater = FrequentUpdater()
@State var tapCount = 0
var body: some View {
VStack {
Text("\(UUID().uuidString)")
Button(action: {
self.tapCount += 1
}) {
Text("Tap count: \(tapCount)")
}
}
}
}
- 위 코드에 버튼을 탭하면 그때부터 uuid가 계속 변경되는 것을 확인 할 수 있음.
- 스트래스 테스트용 코드
- Cmt + I 로 Instrument 를 실행하면 SwiftUI 아이콘이 추가되있음. 그거 실행
- 'View Body' - 뷰가 얼마나 많이 만들어지고, 뷰 생성에 시간이 얼마 소요되는지를 보여줌
- 실제로 보면 뷰가 병합되있는것을 볼 수 있음, VStack 같은게 보일거라는 기대는 하지마세요 라고...
- 시간은 min/max, avg 등등이 있어서 좋음
- 'View Properties' - 뷰들에 어떤 프로퍼티가 있는지, 시간에 따라 어떻게 변화되는지 보여줌
- 'Core Animation Commits' - Core Animation 이 얼마나 commit 되었는지 보여줌
- 'Time Profiler' - function call 이 시간이 얼마나 소요됐는지 보여줌
Tips & Tricks
- resume the live preview - Option + Cmd + P 로
- @State 는 private 으로 만들자
- 디자인 보기 위해서 TextField나 Slider 등에 binding 될 변수를 넣어주는게 귀찮다면 .constant 를 활용하면 됨
TextField(.constant("Hello"))
.textFieldStyle(.roundedBorder)
Slider(value: .constant(0.5))
- 10개 제한되는 뷰를 더 넣고 싶다면, Group으로 넣으면 됨
- UIColor 말고 Color.red는 symantic color 라서 순수 red가 아님, light / dark mode 에 따라서 변하는 색.
- 프리뷰에서 Cmd + Click 해서 Extract View 라던가 다양한 기능을 쓸 수 있음
TabbedView 사용하기
- TabbedView
Push/Pop programmatically
ViewModifier 만들기
- https://alejandromp.com/blog/2019/06/09/playing-with-swiftui-buttons/
- https://alejandromp.com/blog/2019/06/22/swiftui-reusable-button-style/
struct MyButtonStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.yellow)
.cornerRadius(5)
}
}
extension View {
func myButtonStyle() -> some View {
Modified(content: self, modifier: MyButtonStyle())
}
}
Button(action: doSomething) {
VStack {
Image(systemName: "rectangle.grid.1x2.fill")
Text("Vertical Button!")
}
.myButtonStyle()
}