-
[SwiftUI] 1. Creating and Combining ViewsiOS Dev/SwiftUI 2023. 3. 20. 01:01반응형
https://developer.apple.com/tutorials/swiftui/creating-and-combining-views
Creating and Combining Views | Apple Developer Documentation
This tutorial guides you through building Landmarks — an app for discovering and sharing the places you love. You’ll start by building the view that shows a landmark’s details.
developer.apple.com
Apple 공식 튜토리얼을 따라해보면서 SwiftUI를 시작해보자.
Section 1 : 새로운 프로젝트 만들기 & Canvas 둘러보기
Xcode를 띄워서 새로운 프로젝트를 만들자.
LandmarksApp.swift 파일로 가서 내용을 보자.
import SwiftUI @main struct LandmarksApp: App { var body: some Scene { WindowGroup { ContentView() } } }
@main 은 app의 엔트리를 의미한다. 즉 @main이 달려있는 곳부터 앱이 시작된다는 말이다.
LandmarksApp은 struct이다. App 프로토콜을 채택하고 있다. SwiftUI는 거의 모든 것이 struct로 이루어져 있는데, 뷰 속성을 부여할 때 체이닝 기법을 자주 사용해서 그런 것 같다.
some 키워드는 opaque 타입을 나타낸다. 조금 있다가 조금 더 자세히 설명한다.public protocol App { associatedtype Body : Scene @SceneBuilder @MainActor var body: Self.Body { get } ... }
App 프로토콜은 body 변수를 갖고있다. Scene 프로토콜을 채택하는 타입인 Body를 타입으로 갖고 있다.
보다 보면 계속 @ (at sign)이 나오는데, 이는 Attribute임을 의미한다.다음, ContentView.swift 내용을 보자. 원래 있던 내용을 다음과 같이 바꿔주자.
import SwiftUI struct ContentView: View { var body: some View { Text("Hello, world!") .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
여기서도 some 키워드들이 보인다. some은 프로토콜을 opaque 타입(역 제네릭 타입)으로 변환시켜준다. View는 분명 프로토콜인데 body 변수에서 타입으로 사용하고 있다. 다른 예시를 들어볼까?
var a: String = "Hello World!" // 가능 var b: StringProtocol // ???????
some View를 쓰는 순간 리턴값이 View를 채택하기만 하면 허용된다.
즉 다음과 같은 코드도 가능.
struct ContentView: View { var body: some View { return Text("Hello, world!") .padding() } }
하지만 return문 없이 다음과 같이 작성하면?
struct ContentView: View { var body: some View { Text("Hello~") Text("Hello, world!") .padding() } }
ContentView에 있는 두가지 View 요소를 전부 인식한다. 이는 body가 @ViewBuilder를 사용하고 있어서 모두를 child로 인식하고 있기 때문이다. @ViewBuilder가 없는 변수에 넣으면 마지막 문장만 리턴한다. 즉, body 말고 다른 변수에 아무것도 없이 저렇게 넣으면 안된다.
아래 있는 ContentView_Previews는 PreviewProvider라는 프로토콜을 채택한다.
@MainActor public protocol PreviewProvider : _PreviewProvider { @ViewBuilder @MainActor static var previews: Self.Previews { get } ... }
이 친구는 @MainActor라는 Property Wrapper를 사용한다. previews를 싱글톤으로 구성하고, 메인 쓰레드에서만 동작하도록 한다.
코드는 다음과 같이 작성한다.
import SwiftUI struct ContentView: View { var body: some View { Text("Hello, SwiftUI!") .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
Section 2. Customize the Text View
이제 Text를 조금 가꿔보는 시간이다. Inspector로도 뷰를 수정할 수 있고, 수정하면 코드로도 반영이 되는 시스템을 소개하고 있다. Inspector로 뷰를 수정하는 것은 타겟을 my mac으로 했을 때만 가능하다. 시뮬레이터로 아이폰 등을 선택하면 안된다.
Text를 들어가서 보면 @frozen이 붙어있다. 이는 말 그대로 얼어붙었다라는 뜻이다. 더 이상 해당 문맥에서 @unknown이 발생하지 않는다는 것을 알려준다. 찾아보니 최적화를 위해 붙인 것 같다고 한다.
체이닝을 열심히 사용하고 있는 것 같은데, 메모리에 타격이 없을지 실험해보고 싶었다.
struct ContentView: View { var body: some View { Text("Turtle Rock") .font(.title) .foregroundColor(.green) .font(.title) .font(.title2) .foregroundColor(.red) .font(.title3) .font(.title) .font(.title2) .foregroundColor(.blue) .font(.title3) } }
이렇게 해놓고 돌려봤는데 딱히 의미있는 메모리 변화는 없었다. 컴파일러가 최적화를 잘 해두었던지, @frozen의 역할인지는 나중에 커스텀 컴포넌트를 만들어서 한번 더 실행 해보아야겠다.
체이닝을 할 수 있는 이유는 모든 속성들의 리턴값이 Text이기 때문이다. Text를 받아서 가공한 다음 Text를 내보내는 방식이라서 원하는대로 체이닝을 할 수 있다.
Equatable을 채택하고 있어서 == 연산자 재정의부분을 보았더니 Text끼리 비교하지만 구현부는 생략되어있다. 내부의 모든 프로퍼티가 Equatable을 채택하고 있기 때문에 생략한 것으로 보인다.
코드는 다음과 같이 만들고 넘어가자.
import SwiftUI struct ContentView: View { var body: some View { Text("Turtle Rock") .font(.title) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
Section 3. Combine Views Using Stacks
stack으로 다양한 뷰들을 그룹으로 묶을 수 있다. 수평적으로, 수직적으로, back-to-front으로 묶을 수 있다.
import SwiftUI struct ContentView: View { var body: some View { VStack { Text("Turtle Rock") .font(.title) HStack { Text("Joshua Tree National Park") .font(.subheadline) Spacer() Text("California") .font(.subheadline) } } .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
VStack은 어떻게 구현되어있을까?
@frozen public struct VStack<Content> : View where Content : View { @inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) ... }
음.. 제네릭으로 Content라는 타입을 가져오는데, 이는 View를 채택하고 있어야 한다.
@inlinable은 컴파일러 최적화를 위해 사용되는 것이다. 해당 함수 내용을 호출부 대신에 그냥 집어 넣는다고 생각하면 된다.
init으로 alignment와 spacing을 주며, 마지막 요소는 @ViewBuilder를 채택한 클로져가 위치한다.
alignment를 HorizontalAlignment로 주는 것을 주목해야 한다. 즉 수직 방향으로 자라는 stack에서 정렬 방향은 수평선 기준 어느 위치에 갖다 놓을 건지를 결정하는 것이다. 그럼 안봐도 HStack에선 VerticalAlignment를 주겠군..
첫번째 두번째 파라미터가 초기값을 가지고 있고, 마지막에 클로져가 위치하므로 자연스럽게 VStack(content: ){}이 아닌 그냥 Vstack{}으로 줄일 수 있다.
좀 더 복잡하게 쓰면 다음과 같은 코드가 된다.struct ContentView: View { var body: some View { VStack(content: { Text("Turtle Rock") .font(.title) HStack { Text("Joshua Tree National Park") .font(.subheadline) Spacer() Text("California") .font(.subheadline) } }) .padding() } }
그냥 맘 편하게 생략하도록 하자.
Section 4. Create a Custom Image View
이미지를 추가해보자. 먼저 SwiftUI View 파일을 새로 만들자. 이름은 CircleImage.swift
다음 에셋을 만들자.
mosu라는 이름으로 만들었다. (공식 홈페이지 asset에서 가져오는 것 추천)
코드로 이미지를 가져와보았다.
import SwiftUI struct CircleImage: View { var body: some View { Image("mosu") } } struct CircleImage_Previews: PreviewProvider { static var previews: some View { CircleImage() } }
Image를 안클릭해볼수 없다. 근데 들어가봤더니 Equatable 채택한거 말고 전부 extension으로 숨어있다 ㅠ
다음과 같이 코드를 변경해주자.
import SwiftUI struct CircleImage: View { var body: some View { Image("mosu") .clipShape(Circle()) .overlay { Circle().stroke(.white, lineWidth: 4) } .shadow(radius: 7) } } struct CircleImage_Previews: PreviewProvider { static var previews: some View { CircleImage() } }
Section 5. Use SwiftUI Views From Other Frameworks
MapKit 프레임워크를 사용해서 지도를 표시해보자.
먼저 MapView.swift파일을 만들자.
import MapKit 으로 프레임워크를 포함시킨다.
@State private var region = MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 34.011_286, longitude: -116.166_868), span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2) )
@State가 나오는데, 이건 SwiftUI에서의 대표적인 Property Wrapper이다. 위에서도 말했듯, @propertyWrapper를 붙인 구조체, 클래스 등은 attribute로 사용할 수 있다.
코드를 까보자.
@frozen @propertyWrapper public struct State<Value> : DynamicProperty { public init(wrappedValue value: Value) public init(initialValue value: Value) public var wrappedValue: Value { get nonmutating set } public var projectedValue: Binding<Value> { get } }
DynamicProperty는 update()를 구현해주어야한다는 내용이 있다.
wrappedValue는 get할 수 있고, mutating 할 수 없는 set이 있다.
projectedValue는 Binding<>으로 되어있다.기본적으로 wrappedValue으로 값을 관리한다는 것을 알 수 있다.
하지만 특정 상황에서는 projectedValue를 가져간다.먼저 @State는 말 그대로 상태를 유지하겠다는 attribute이다. 특이하게도 이 친구는 무조건 call by reference 방식으로 값을 갱신한다.
@Binding은 값이 바뀌면 뷰를 다시 랜더링하겠다는 변수도 함께 갖고 있다.@State는 Binding으로 된 projectedValue를 갖고 있는데, 이 친구를 데리고 값 변경을 시도하면, @State의 wrappedValue가 바뀌고, 뷰를 다시 랜더링하겠다는 신호를 보내게 된다.
wrappedValue를 사용하고 싶으면 그냥 해당 변수를 사용하면 되고, projectedValue를 사용하고 싶으면 해당 변수 앞에 $ 달러 표시를 붙여주어야 한다.
따라서 MapView.swift 코드는 다음과 같이 작성한다.
import SwiftUI import MapKit struct MapView: View { @State private var region = MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 34.011_286, longitude: -116.166_868), span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2) ) var body: some View { Map(coordinateRegion: $region) } } struct MapView_Previews: PreviewProvider { static var previews: some View { MapView() } }
Section 6. Compose the Detail View
지금까지 만든 각각의 컴포넌트들을 합치는 작업을 진행한다.
import SwiftUI struct ContentView: View { var body: some View { VStack { MapView() .ignoresSafeArea(edges: .top) .frame(height: 300) CircleImage() .offset(y: -130) .padding(.bottom, -130) VStack(alignment: .leading) { Text("Turtle Rock") .font(.title) HStack { Text("Joshua Tree National Park") .font(.subheadline) Spacer() Text("California") .font(.subheadline) } .font(.subheadline) .foregroundColor(.secondary) Divider() Text("About Turtle Rock") .font(.title2) Text("Descriptive text goes here.") } .padding() Spacer() } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
홈페이지에 나온 그대로 따라하면 된다.
완성된 화면이다. 생각보다 배울게 많다.
퀴즈
퀴즈를 한번 풀어보자.
답은? 순서대로 2313
반응형'iOS Dev > SwiftUI' 카테고리의 다른 글
[SwiftUI] 6. Composing Complex Interfaces (0) 2023.05.04 [SwiftUI] 5. Animating Views and Transitions (0) 2023.05.04 [SwiftUI] 4. Drawing Paths and Shapes (2) 2023.04.30 [SwiftUI] 3. Handling User Input (0) 2023.03.30 [SwiftUI] 2. Building Lists and Navigation (0) 2023.03.28