ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SwiftUI] 1. Creating and Combining Views
    iOS 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

    반응형

    댓글

Designed by Tistory.