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