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 ๋ž‘ ๊ฐ™์ด ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋‚˜ ์•Œ์•„๋ณธ๋‹ค

    • SwiftUI ์—์„œ XIB / Storyboard์˜ view๋ฅผ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • storyboard ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•จ
      • viewController ๋กœ ์‚ฌ์šฉ์€ ์•ˆ๋˜๊ณ , viewController์˜ view๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ์Œ
    • 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 { }

ํ™œ์šฉํ•ด์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

  • ๊ทผ๋ฐ ์‹ค์ œ ์‹คํ–‰ํ•ด๋ณด๋ฉด print ๊ฐ€ ์‹คํ–‰์ด ์•ˆ๋จ. debug๋„ ์•ˆ๋จ. ์›๋ž˜ ์•ˆ๋˜๋Š”์ง€ ํ™•์ธ ํ•„์š”

  • onDisappear() ๋Š” ํ˜ธ์ถœ์ด ์•ˆ๋จ - xcode 11 beta 1 ์—์„œ๋Š” ์•ˆ๋˜๋Š” ๋“ฏ

@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