DADAHAE's Log
비싼 장난감 가지고 노는 중 (❁´▽`❁)*✲゚*
[TIL] 22-10-19: SwiftUI 기초(Stack, Frame, State)
22-10-19 수요일
6주차

 

 

 

 

 

 

🦢 SwiftUI

오늘도 시작!

 

📒 Review: Custom View

  • Spacer
    • 공간 차지
  • Text
  • Image
  • Dynamic Type
    • 글자 크기를 조절할 수 있다. 휴대폰의 폰트 크기 설정을 생각하면 된다.
    • 이것에 따라 유동적으로 글의 크기를 맞추려면, 애플에서 정한 Title, subheadline과 같이 정해진 크기를 사용하도록 하자.
  • padding
  • VStack, HStack
  • Custom View
    • struct로 따로 뽑아내기
    • View 프로토콜을 채택하면 body 프로퍼티를 필수적으로 만들어야 한다.
    • body에도 한가지 내용을 리턴하는 것이므로 일반적으로 return 키워드는 생략하고 적는다.
    • 내용이 길어지면 별도의 파일로 분리하기도 한다.
  • Preview Canvas
    • Preview를 위한 구조체가 존재한다. 이 부분도 코드를 적용하여 원하는 그림을 프리뷰에 보여줄 수도 있다.
    • 여러개의 프리뷰 화면을 한꺼번에 보여줄 수도 있다.
  • Custom Modifier
    • 한꺼번에 수정자를 설정하고 싶다.
    • struct로 따로 뽑아내기
    • ViewModifier 프로토콜을 채택하면 따라야하는 내용이 있다. body를 넣긴하는데. 어제는 func로만 만들었다. 일단 따라해보자.
  • Button
    • 만드는 방법이 크게 4가지가 있었다.
    Button("안녕하세요") {   // 후행 클로저
    	print("Hello")
    }
    
    Button(action: {      // action에 후행 클로저 내부 내용과 같음
    	print("Hello")
    }) { 
    	Text("안녕하세요")     // label 자리에 Text 뷰로 들어감
    }
    
    Button(action: {      // Image + Text 버튼
    	print("Hello")
    }) { 
    	HStack {
    		Image(systemName: "globe")
    		Text("안녕하세요")
    	}   
    }

 

 

 


 

 

 

 

✔ Custom View 생성하기 +

어제 했던 Custom View를 이어서 진행한다.

 

1️⃣ onAppear, onDisappear

  • onAppear
  • onDisappear

 

 

2️⃣ Custom Container

하위 뷰가 레이아웃에 포함되는 시점에 하위 뷰에 포함될 뷰를 동적으로 지정할 수 있다.

 

ViewBuilder 클로저 속성을 이용하여 Custom Container View를 생성할 수 있다.

  • closure 형태
  • 여러 하위 view로 구성된 custom view를 만드는데 사용
  • content view들을 받아서 동적으로 만들어진 단일 view로 반환한다.
struct NamesView: View {
    var body: some View{
        MyVStack {
            Text("123")
            Text("456")
            Text("789")
            Text("567")
						HStack {
	            Image(systemName: "star.fill")
							Image(systemName: "star")
						}
        }.modifier(StandardName())
    }
}

struct MyVStack<Content: View>: View {
    let content: () -> Content
    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }
    var body: some View {
        VStack(spacing: 20) {
            content()
        }
        .foregroundColor(Color.orange)
    }
}

MyVStack 뷰는 ViewBuilder 속성을 사용하여 구현되었다.

MyVStack의 body에는 VStack의 선언부를 포함한다. 그러나 정적 뷰를 넣는 대신에 하위 뷰들은 초기화 메서드 init에 전달한다.

  • 여기서 정적 뷰는 일반적인 뷰들 Text, Image 등을 말한다. 이미 정해서 넣어버리면 못바꾸니까 정적뷰라고 표현하는거지.

초기화 메서드에 전달된 content, 즉 하위 뷰들은 ViewBuilder에 의해 처리되어서 VStack의 하위뷰로 들어가게 된다…

  • Text(”123”)이 MyVStack의 초기화 메서드로 전달되어서 content에 담아진다. 그렇게 담아진 뷰는 MyVStack의 VStack의 하위 뷰로 포함된다.
  • escaping이 쓰인 이유는, () → Content 타입의 변수 content에 클로저를 담을 수 있게 하기 위해서이다.
    • 클로저는 한번 사용되면 다시 잡을 수 없는데 (저장되지 않으니까) 그런 클로저를 메모리 상에 올리기 위해서 ‘탈출(?)’ 시켜주고 변수에 할당할 수 있게 하는거다. 그러면 content에 할당된 클로저, 즉 Text는 구조체 내에서 content에 담겨 필요할 때 사용할 수 있게 되는 거다.

 

 

 


 

 

✔ Stack, Frame

 

1️⃣ Stack

  • VStack : 수직
  • HStack : 수평
  • ZStack : 중첩

 

2️⃣ Space, alignment, padding

  • Space
    • 뷰 사이의 공간을 추가 가능
    • 스택안에서 사용할 경우, 배치된 뷰들의 간격 제공하기 위해 스택 방향에 따라 유연하게 확장/축소 된다.
  • alignment, spacing
    • 스택 선언될 때 지정
    • 간격 지정 가능
  • padding
    • 뷰 주변의 간격
VStack(alignment: .center, spacing: 15) {
	Text("Hi")
	Spacer()
	Text("My name is dadahae")
		.padding(.top, 10)
}.padding()

 

3️⃣ Group

원래 Container View에는 자식 view가 10개밖에 안들어간다. 근데 개발하다 보면 10개 이상 들어가야 할 수도 있다. 그때는 어떻게 할까? (Contianer View → Stack 같은 애들)

view 여러개를 Group으로 묶어 사용하면… Contianer View에 10개 이상의 View를 넣을 수 있다.

예를 들어 6개의 Text View가 있는 Group 2개를 VStack에 넣었다고 하자. 그럼 해당 VStack에는 총 12개의 Text View가 있는 것이다!

Group안에 10개 이내로만 있으면 된다. Group도 ‘컨테이너’니까… 그것이 ‘컨테이너’니까..!

Group {
	Text("I")
	Text("I")
	Text("I")
	Text("I")
	Text("I")
	Text("I")
}

 

4️⃣ Text Line Limit, Layout Priority

텍스트를 여러줄로 표현해야 하는 경우, lineLimit() 수정자를 사용하면 몇줄까지 나올 지 강제할 수 있다.

뷰들의 우선순위 정보가 없으면 스택은 어떤 뷰가 우선인지 모른다. 이때 layoutPriority() 수정자를 사용한다. 이것으로 통해서 텍스트가 늘어날 경우 우선순위에 따라 어떤 것을 우선으로 보일지 결정한다.

HStack {
	Image(systemName: "globe")
	Text("Time Limit")
	Text("You can go now.")
		.layoutPriority(1)
}
.font(.largeTitle)
.linelimit(1)

 

5️⃣ SwiftUI Frame

SwiftUI는 조절가능한 frame 수정자를 제공한다.

  • width, height, alignment
  • minWidth, maxWidth, minHeight, maxHeight
    • 최대 최소 크기를 정해두고 대응할 수 있도록
    • .infinity : 최대한!! 늘려줘 혹은 줄여줘
  • .edgeIgnoringSafeArea() 수정자를 사용하면 safe area 무시함
Text("Hello World")
	.font(.largeTitle)
	.border(Color.red)
	.frame(minWidth: 100, maxWitdh: .infinity, minHeight: 100, maxHeight: .infinity, alignment: .center)

// min을 0으로, max를 무한으로 설정하여 사용 가능한 영역을 모두 차지하도록 할 수도 있다.

 

6️⃣ frame, GeometryReader

view들을 담고 있는 컨테이너의 크기에 따라 조절되도록 구현할 수 있다.

  • GeometryReader로 view를 감싸고, 컨테이너 크기를 식별할 수 있는 reader를 이용하면 된다.
    • reader는 프레임 크기를 계산하는데 사용하고, 클로저 변수로 쓴다.
GeometryReader { geometry in 
	VStack {
		Text("HI")
			.frame(width: geometry.size.width / 2, 
						height: geometry.size.height / 4 * 3)
		Text("BYE")
			.frame(width: geometry.size.width - 100, 
						height: geometry.size.height / 4)
		}
}

 

 

 

 


 

 

 

✔ State, Observable Object, Enviroment Object

지금까지는 정적인 데이터로 화면을 다뤄봤다. 이제 동적인 데이터를 작업해보자!

이들 모두 사용자 인터페이스를 구성하는 뷰의 코드에서 직접 업데이트하지 않는다. 대신에 뷰와 바인딩된 상태 객체가 업데이트되면 그에 따라 자동으로 뷰가 업데이트 된다.

 

1️⃣ state property

Apple Developer Documentation

 

@state

state(상태)에 대한 가장 기본 형태로, 뷰 레이아웃의 현재 상태를 저장하기 위해서 사용된다.

  • 간단한 데이터(String, Int…) 타입을 저장하기 위해 사용
  • 왜 골뱅이야? 이거 뭔데요 → 너가 배운 Property Wrapper 입니다만?

 

  • 구조체 내에서만 활용될 값이다. 그러므로 private 붙여야 한다!
더보기

private 안붙이면 어떻게 되는데오?

 

Don’t initialize a state property of a view at the point in the view hierarchy where you instantiate the view, because this can conflict with the storage management that SwiftUI provides. To avoid this, always declare state as private, and place it in the highest view in the view hierarchy that needs access to the value.

 

  • state property를 선언했다면 레이아웃에 있는 뷰와 바인딩할 수 있다.
    • 상태 프로퍼티와의 바인딩 → 프로퍼티 앞에 $ 표기
struct ContentView: View {

    @State private var userName: String = ""  // state property
    
    var body: some View {
        VStack {
            TextField("이름을 입력하세요", text: $userName)  // binding
            Text(userName)
        }
    }

}

 

 

  • state property에 변화 → 뷰 계층구조는 SwiftUI에 의해 다시 rendering
  • 단방향 프로세스
    • 상태가 변하면 레이아웃에 있는 뷰들도 변경
    • Text(userName)에서 userName을 사용하므로 뷰가 업데이트 되어야 한다.

 

더보기

엥 근데 Text(userName)에서는 달러($) 안쓰나요..?

 

달러($)가 없이 사용되었다는 것은 이제 상태 프로퍼티에 할당된 값을 참조하려고 하기 때문이다!

 

struct ContentView: View {
    @State private var wifiEnable: Bool = false
    @State private var userName: String = ""
    
    var body: some View {
        VStack {
            Text(userName)
                .font(.largeTitle)
            TextField("내가 누구?", text: $userName)
            
            Toggle(isOn: $wifiEnable) {
                Text("Wi-Fi 가능한가요?")
            }
            HStack {
                Image(systemName: wifiEnable ? "wifi" : "wifi.slash")
                Text(wifiEnable ? "휴~ 유튜브 봐야지" : "저... 인터넷이 끊겼는데요...")
            }
        }
        .padding()
    }
}

 

 

 

 

 

2️⃣ state binding

@binding

하위 뷰에서 상태 프로퍼티를 사용하고 있는데, 이 하위 뷰를 분리했을 때에도 상태 프로퍼티에 접근할 수 있게 하려면 @Binding 프로퍼티 래퍼를 분리한 하위 뷰에 선언하면 해결된다.

struct WifiView: View {
	@Bidning var wifiEnabled: Bool
	
	var body: some View {
		Image(systemName: wifiEnabled ? "wifi" : "wifi.slash")
	}
}

 

하위 뷰가 호출 될 때 상태 프로퍼티에 대한 바인딩을 전달한다. 달려 표시를 붙여준다.

WiFiView(wifiEnabled: $wifiEnabled)

 

  Comments,     Trackbacks