SwiftUI Study - kirseia/study GitHub Wiki

SwiftUI 공부 (with hackingwithswift.com)

만들어보기

  • TabbedView를 포함하기
  • 첫 번째 탭은 List 를 포함하고, Detail View를 가지고 있음. - List <-> Detail view 사이에 @ObjectBinding 로 데이터 전달
  • 두 번째 탭은 Setting, Setting에 있는 어떤 값을 @EnvironmentObject 로 저장해서 list/detail 에서 사용할 수 있게 하기

UIKit -> SwiftUI 만들기

  • 모든 UIKit을 SwiftUI로 대체할 순 없음
  • UICollectionView / UITextView 는 대체 불가

목표

  • 기본적인 UI 사용을 익혀서 뷰를 구성할 수 있다

  • 애니메이션 처리 해본다

  • 기본 Storyboard / xib 랑 같이 사용 할 수 있나 알아본다

  • 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")
        }
    }
}
  • 왜 인지 동작 안함.

// https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-different-layouts-using-size-classes

* 뷰를 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 { }

활용해서 사용 가능

@ObjectBinding, @State, @EnvironmentObject 차이?

// https://www.hackingwithswift.com/quick-start/swiftui/whats-the-difference-between-objectbinding-state-and-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 만들기

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()
}

Apple Swift Tutorials