State

: 값이 변하면 View를 새로 그려주는 역할을 수행

State 란 무엇인가?

1. State Property store values that the view depends on

⇒ 뷰가 의존하는 상태 속성 저장값

2. State Property represent values that can change

⇒ State Property는 변경할 수 있는 값을 나타냅니다.

3. State Property typically declared in the view that nedds them

⇒ State Property는 일반적으로 이를 추가하는 뷰에서 선언됩니다.

예제 코드

  • State 속성을 사용한 toggleValue 의 변화에 따라 Text가 변하는걸 볼수 있다

Binding

: State의 값을 받아오거나 변경할 때 사용 ( View간 양방향 소통 )

$ 기호를 사용하여 속성을 정의한다

Binding이란 무엇인가?

1. Put a ‘$’ infront of a State Property reference in your view code (Not in the declaration) to turn it into a two way, Read/Write relationship

⇒ view 코드(선언 아님)에서 State Property 참조 앞에 '$'를 넣어 양방향 읽기/쓰기 관계로 전환합니다.

2. @Binding is a property wrapper to indicate that it expects a binding to be passed to the view when an instance of the view is created

⇒ @Binding은 뷰의 인스턴스가 생성될 때 뷰에 바인딩이 전달될 것으로 예상함을 나타내는 속성 래퍼입니다.

예제 코드

  • State를 선언한 View
struct ParentView: View {
    @State private var toggleValue = true
    var body: some View {
        SubView(toggleValue: $toggleValue)
    }
}
  • Binding으로 State변수를 받는 SubView
struct SubView: View {
    @Binding var toggleValue: Bool
    
    var body: some View {
        if toggleValue == false {
            Text("Toggle is False")
        } else {
            Text("Toggle is True")
        }
        
        Button(action: {
            self.toggleValue.toggle()
        }) {
            Text("토글 버튼")
        }
    }
}
  • @Binding 을 명시함 으로써, State변수를 받아옴을 명시
  • 부모 View 에서 Binding으로 값을 넘겨 줄때는 $ 기호를 사용

Observable Object

: 여러 View에서 의존하기를 원할때 사용하는 Object

Observable Object를 사용하기 위한 절차

  1. Class가 Observable Object 프로토콜을 채택해야함
  2. 프로토콜을 채택함으로써 변경하상에 대해 Class의 객체를 관찰하거나 수신
  3. @Published를 선엄하여 변경사항을 저장할 속성을 명확하게 명시 ( Class 내의 개체에 명시 )
  4. @Observed Object 속성을 사용하여 관찰가능한 개체에 대한 참조를 저장

예제 코드

  • 여러 View에서 의존 할 Class 생성
class Profile: ObservableObject {
    @Published var isLogined = false
}

⇒ Class에 Observable Object를 채택

⇒ @Published 를 사용함 으로써 변하는 값을 감시할 변수 명시

struct LoginView: View {
    @ObservedObject var profile = Profile()
    
    var body: some View {
        if profile.isLogined {
        Text("로그인 완료")
        } else {
            Text("버튼을 눌러 로그인 해주세요")
        }
        
        Button(action: {
            profile.isLogined.toggle()
        }){
            Text("버튼")
        }
    }
}

⇒ @ObservedObject 를 사용함 으로써, 변경 사항을 감시할 개체 인스턴스 생성

하지만 이러한 방식을 사용하면 하위 View 모두에게 동일 인스턴스를 전달 할 수 없다 다음 예를 보자

버튼을 클릭하여 로그인을 하면, 로그인 사용자의 닉네임을 보여줄 것이다.

  • LoginView
struct LoginView: View {
    @ObservedObject var profile = Profile()
    
    var body: some View {
        if profile.isLogined {
        Text("로그인 완료")
        } else {
            Text("버튼을 눌러 로그인 해주세요")
        }
        LoginCheckView()
        Button(action: {
            profile.isLogined.toggle()
        }){
            Text("버튼")
        }
    }
}
  • LoginCheckView
struct LoginCheckView: View {
    @ObservedObject var profile = Profile()
    
    var body: some View {
        if profile.isLogined {
            Text("로그인한 사용자 : Chikong.")
        } else {
            Text("로그인 정보가 없습니다.")
        }
    }
}

코드를 작성하고 앱을 실행하면

  • 하위 View의 값이 변하지 않는것을 확인 할 수 있다

이러한 현상이 나타나는 이유는 다음 그림을 살펴보자

  • 다음과 같이 각 View에 새로운 Class 인스턴스를 생성 했기 때문에 각각 다른 인스턴스를 참조 하고 있다.
  • 그렇기 때문에 두개의 인스턴스는 다른 것으로 인식하게 되고, 연관된 값을 변경 하거나 감시할 수 없다

EnvironmentObject

: 하위 View 모두에게 동일한 객체 인스턴스를 사용

위의 예시를 해결할 수 있는 방법이 EnvironmentObject 이다. EnvironmentObject를 사용하면 동일 인스턴스를 하위 View 모두에게 전달 할 수 있다.

위의 문제를 해결할 예제 코드를 보자

  • MainView
import SwiftUI

@main
struct DataFlowApp: App {
    var body: some Scene {
        WindowGroup {
            LoginView()
                .environmentObject(Profile())
        }
    }
}

⇒ 제일 상위 Main View에서 .environmentObject(Profile()) 를 이용해 객체 인스턴스를 넘겨주었다. 이제 MainView의 하위 View에서 동일한 인스턴스를 참조할 수 있다.

  • LoginView
struct LoginView: View {
    @EnvironmentObject var profile: Profile
    
    var body: some View {
        if profile.isLogined {
        Text("로그인 완료")
        } else {
            Text("버튼을 눌러 로그인 해주세요")
        }
        LoginCheckView()
        Button(action: {
            profile.isLogined.toggle()
        }){
            Text("버튼")
        }
    }
}
  • LoginCheckView
struct LoginCheckView: View {
    @EnvironmentObject var profile: Profile

    var body: some View {
        if profile.isLogined {
            Text("로그인한 사용자 : Chikong.")
        } else {
            Text("로그인 정보가 없습니다.")
        }
    }
}

⇒ LoginView 와 LoginCheckView에 @EnvironmentObject var profile: Profile 부분을 보자.

@EnvironmentObject 속성을 명시 했고 같은 인스턴스를 참조함을 구체적으로 적어 주었다

 

⇒ 같은 인스턴스를 참조하기 때문에 로그인상태에 따라 값이 변함을 확인 할 수 있다.

이처럼 하위 View에 동일 인스턴스를 참조해야 할때, 필요한 View에 명시해주면 사용할 수 있다.

 

 

참고 유튜브 : https://www.youtube.com/watch?v=jD6c9y8CFGQ

Nano Challenge 를 진행하는데 횡 스크롤을 구현해야 해서 ScrollView를 공부해 보았다

공식문서 내용

스크롤 뷰는 스크롤 가능한 콘텐츠 영역 내에서 콘텐츠를 표시합니다. 사용자가 플랫폼에 적합한 스크롤 제스처를 수행할 때, 스크롤 보기는 기본 콘텐츠의 어떤 부분이 보이는지 조정합니다. ScrollView는 수평, 수직 또는 둘 다 스크롤할 수 있지만 줌 기능을 제공하지는 않습니다.

  • 어..? HIG에는 적절한 줌 기능을 제공하라고 했는데..? 그건 페이지 컨트롤을 사용할때 한정인가보다.
  • 아직까지는 줌 기능이 필요하지 않으니 추후에 스크롤과 줌 기능이 필요하면 그때 다시 찾아보자.
  • 일단 기본적으로 ScrollView는 줌 기능을 제공하지 않음!

정의

struct ScrollView<Content> where Content : View

→ 정의를 보면 View를 반환하는 구조체로 되어있다.

이 말은 우리는 그냥 ScrollView{ ... return view } 의 형태로 구현하면 된다!

ScrollView 사용해보기

ScrollView() {
	VStack {
		ForEach (0..<20) { i in
			Text("\\(i)")
		}
	}
}

→ 결과

  • ScrollView의 기본 Default 값은 vertical 이라는것을 알수 있다.
  • 스크롤 뷰 내의 구성 요소들을 수직으로 정렬하기 위하여 VStack 에 요소를 넣어주고 구성하면 된다!
  • 내가 필요한 것은 횡 스크롤 이므로 구성 요소를 HStack으로 구성하면 되려나?
ScrollView() {
	HStack {
		ForEach (0..<20) { i in
			Text("\\(i)")
		}
	}
}

→ 결과

응..? 뭔가 이상하다 스크롤도 안될 뿐더러 요소들이 아주 이상하게 배치되어 있다

  • 횡 스크롤을 사용하려면 ScrollView 에 파라미터로 .horizontal 을 설정하자!
ScrollView(.horizontal) {
	HStack {
		ForEach (0..<20) { i in
			Text("\\(i)")
		}
	}
}

→ 결과

 

  • 원하는 대로 동작이 나왔다

아!! 스크롤뷰는

  • Default 값으로 수직 스크롤이 되는구나
  • 횡 스크롤을 구성하려면 horizontal 을 넣어줘야 하는구나!
  • 스크롤내의 요소들을 스크롤 방향에 따라 구성해야 하는구나!

라는 점을 배웠다

그런데 ScrollView의 컴포넌트 중에 showsIndicators 라는 것이 있다. 이게 뭔가 찾아볼까 하다가 직접 해보며 익히는게 좋을거같아 차이를 확인해 봤다

showsIndicators = true

ScrollView(.horizontal, showsIndicators = true) {
	HStack {
	ForEach (0..<20) { i in
		Text("\\(i)")
		}
	}
}

→ 결과

 

⇒ 변한게 없다..? 그럼 false 도.. 확인해보자

showsIndicators = false

ScrollView(.horizontal, showsIndicators = fasle) {
	HStack {
		ForEach (0..<20) { i in
			Text("\\(i)")
		}
	}
}

→ 결과

⇒ 아! 차이가 보인다

HIG에 명시되어 있는 스크롤 표시기가 이 내용인가 보다!

스크롤이 되었을때, 인디케이터가 표시된다 현재 스크롤이 어느지점쯤에 있는지 보여준다!

정리

  • showsIndicators 는 스크롤 진행 표시기? 정도로 이해하면 될거같다
  • 기본 default값으로 true 가 설정된다
  • false를 설정하면 표시기를 없앨 수 있다

ScrollView를 적용한 결과

아. 뿌듯. 이상.

본 내용은 스윗한 SwiftUI 책에 있는 예제를 공부한 내용입니다.
해당 예제들에 대한 저작권은 BJpublic 에 있습니다.

 

  • 이미지에 frame 속성을 사용해도 이미지 자체의 크기를 변경해 주진 않음
  • Resizable 속성을 사용하여 이미지 크기를 변경할 수 있음
HStack {
	Image("imgFile")     // 이미지 크기는 변하지 않고, 이미지를 포함함 뷰의 크기가 변함
		.frame(width: 50, height: 50)

	Image("imgFile").resizable()    // resizable() 을 사용하여 이미지 크기 자체가 변함
		.frame(width: 200, height: 50)
}

  • frame 은 View의 속성이기 때문에 resizable 과 순서가 중요함!
  • resizable() 을 사용하여 특정 영역만 늘려주는 것도 가능함
  • 단, UIKit 처럼 ResizingMode 를 생략하면 tile 이 아니라 stretch가 기본값이 됨
HStack {
            Image("swift")
            //capInset 매개 변수에 늘어날 영역 지정. resizingMode 생략 시 stretch 적용
                .resizable(capInsets: .init(top: 0, leading: 50, bottom: 0, trailing: 0))
                .frame(width: 150, height: 150)
            
            Image("swift")
                .resizable(resizingMode: .tile)
                .frame(width: 150, height: 150)
        }

 

ContentMode

UIKIt SwiftUI 설명
Scale to Fill Default 값 종횡비와 상관없이 이미지를 늘려 표현
Aspect Fit .scaledToFit() 종횡비를 유지한 채로 표현가능한 최대로 표현
Aspect Fill .scaledToFill() 종횡비를 유지한 채로 표현하며, 이미지가 짤리거나 추가적으로 더 크게 표현

 

HStack(spacing: 30){
						// 기본값
            Image("swift").resizable().frame(width: 100, height: 150)
            
						// UIKit의 Aspect Fit 적용
            Image("swift").resizable()
                .scaledToFit()
                .frame(width: 100, height: 150)

						// UIKit의 Aspect Fill 적용            
            Image("swift").resizable()
                .scaledToFill()
                .frame(width: 100, height: 150)
        }

 

AspectRatio

  • 이미지 비율을 좀더 세부적으로 조정하기 위해 사용하는 수식어
  • 모든 콘텐츠가 적용된 상황에서 추가적으로 비율조정을 하기위해 사용
// CGFloat : 너비 / 높이 를 계산한 비율을 전달
func aspectRatio(_ aspectRatio: CGFloat? = nil, contentMode: ContentMode) -> some View

// CGSize : 너비 와 높이를 각각 설정
func aspectRatio(_ aspectRatio: CGFloat? = nil, contentMode: ContentMode) -> some View

AspectRatio 적용한 예제

HStack(spacing: 30) {
            //scaledToFit 콘텐츠 모드를 적용한 뒤, 너비가 높이보다 1.6배 비율을 가지도록 조정
            Image("swift").resizable()
                .aspectRatio(CGSize(width: 1.6, height: 1), contentMode: .fit)
                .frame(width: 150, height: 150)
            
            // scaledToFill 콘텐츠 모드를 적용한뒤, 너비가 높이보다 0.7배의 비율을 가지도록 조정
            Image("swift").resizable()
                .aspectRatio(0.7, contentMode: .fill)
                .frame(width: 150, height: 150)
                .clipped()
        }

 

ClipShape

  • 이미지를 원하는 모양으로 조정
  • 크기 또한 조정 가능
HStack(spacing: 30) {
            // 원 모양
            Image("swift").clipShape(Circle())
            
            // 이미지 크기보다 사방으로 10씩 크기를 조정한 사각형
            Image("swift").clipShape(Rectangle().inset(by: 10))
            
            // 크기와 위치를 직접 지정한 타원
            Image("swift").clipShape(Ellipse().path(in: CGRect(x: 10, y: 10, width: 80, height: 110)))
        }

 

RanderingMode

  • template : 이미지의 불투명 영역이 가진 본래의 색을 무시하고 원하는 색으로 변경
  • original : 항상 이미지의 본래 색을 유지
  • 렌더링 모드를 생략하면 시스템이 알아서 조정을 하므로, 원치않은 결과가 나오면 렌더링 모드를 지정해야함
HStack(spacing: 30) {
            // 렌더링 모드 생략 -> 시스템이 스스로 결정
            Image("swift")
            
            // 원본 이미지 색상 유지
            Image("swift").renderingMode(.original)
            
            // template 모드 적용
            Image("swift").renderingMode(.template)
        }
        .foregroundColor(.blue) // 자식 뷰 모두에게 일괄 적용

 

SF Symbol

  • 애플에서 직접 만들고 제공하는 이미지들의 모음
  • iOS 13이상부터 사용 가능
  • 벡터 기반의 이미지로, 시스템 폰트로 크기를 조정 할 수도 있음
  • 앱에서 심볼들을 확인 할 수 있으며, systemName: 으로 간단히 사용 가능
HStack(spacing: 30){
            Image(systemName: "star.circle")
            Image(systemName: "star.circle.fill")
        }

  • ImageScale 을 적용하면 크기를 지정 할 수 있다. ( 기본값 → medium )
HStack(spacing: 30) {
            Image(systemName: "star.circle").imageScale(.small)
            Image(systemName: "star.circle")
            Image(systemName: "star.circle").imageScale(.large)
        }

  • font 속성을 이용해서 크기를 조정 할 수도 있다
HStack(spacing: 30) {
            Image(systemName: "star.circle").font(.body)
            Image(systemName: "star.circle").font(.title)
            Image(systemName: "star.circle").font(.system(size: 40))
            Image(systemName: "star.circle").imageScale(.large).font(.system(size: 40))
        }

  • weight 속성을 이용하면 굵기 표현도 가능 하다
HStack(spacing: 30) {
            Image(systemName: "star.circle").font(Font.title.weight(.black))
            Image(systemName: "star.circle").font(Font.title.weight(.semibold))
            Image(systemName: "star.circle").font(Font.title.weight(.light))
            Image(systemName: "star.circle").font(Font.title.weight(.ultraLight))
        }

 

본 내용은 스윗한 SwiftUI 책에 있는 예제를 공부한 내용입니다.
해당 예제들에 대한 저작권은 BJpublic 에 있습니다.

전체적으로 훑어보기

VStack(spacing: 30) {  // 세로 방향으로 뷰를 배열하는 컨테이너 뷰
            Text("폰트와 굵기 설정")
                .font(.title) // 폰트 설정
                .fontWeight(.black) // 폰트 굵기
            
            Text("글자색은 foreground, 배경은 background")
                .foregroundColor(.white)
                .padding()      // 텍스트 주변 여백 설정
                .background(Color.blue)
            
            Text("커스텀 폰트, 볼드체, 이탤릭체, 밑줄, 취소선")
                .font(.custom("Menlo", size: 16))
                .bold()
                .italic()
                .underline()
                .strikethrough()
            
            Text("라인 수 제한과 \\n 텍스트 정렬 기능입니다. \\n 이건 안 보입니다.")
                .lineLimit(2)
                .multilineTextAlignment(.trailing) // 다중행 문자열의 정렬 방식 지정
                .fixedSize() // 주어진 공간의 크기가 작아도 텍스트를 새략하지 않고 표현하도록 설정
            
            // 2개 이상의 텍스트를 하나로 묶어서 동시에 적용
            (Text("자간과 기준선").kerning(8) //자간
             + Text(" 조정도 쉽게 가능합니다.").baselineOffset(8)) // 기준선
            .font(.system(size: 16))

 

Text 수식어 적용의 순서의 중요성

  • Text 와 View 의 font, foregroundColor 의 정의
extention Text {
	// Text와 View 모두 있는 수식어
	public func font(_ font: Font?) -> Text
	public func foregroundColor(_ color: Color?) -> Text
	
	// Text에만 있는 수식어
	public func bold() -> Text
	public func italic() -> Text
}

extention View {
	// Text와 View 모두 있는 수식어
	public func font(_ font: Font?) -> some View
	public func foregroundColor(_ color: Color?) -> some View

	// View 에만 있는 수식어
	public func padding(
		_ edges: Edge.Set = .all,
		_ length: CGFloat? = nil
	) -> some View
}
  • Text에도, View에도 font 와 foregrountColor 가 정의되어 있다
  • 단, 반환 타입이 다름을 주시하자
  • View 에만 있거나, Text에만 있는 경우 순서가 중요하다

잘못된 순서로인한 변화 예제

Text("SwiftUi")
	.font(.title) // Text - 호출자의 타입이 Text
	.bold()       // Text
	.padding()    // View - padding 수식어 호출 이후로는 Text가 아닌 View 반환

Text("SwiftUi")
	.bold()       // Text
	.padding()    // View
	.font(.title) // View - 동일한 font 수식어를 호출해도 호출자에 따라 반환타입 다름

Text("SwiftUi")
	.padding()    // View
	.bold()       // 컴파일 오류 - View에는 bold 가 정의되어 있지 않기 때문
	.font(.title) 

Text("SwiftUi")
	.font(.title) // View
	.padding()    // View
	.bold()       // 컴파일 오류 - View에는 bold 가 정의되어 있지 않기 때문

잘못된 순서로 인한 변화 예제2

// 1번
Text("🐶🐱🐭🐹🐼").font(.largeTitle)
	.background(Color.yellow) // 배경 색상지정이 Text 크기에 따라 우선 적용
	.padding()                // 배경 색상이 적용된 Text에 padding 적용

// 2번
Text("🐶🐱🐭🐹🐼").font(.largeTitle)
	.padding()                // Text에 Padding 을 우선 적용!
	.background(Color.yellow) // padding 이 적용된 부분에 배경색상 지정

  • 1번

  • 2번

  • 보이는 것과 같이 순서에 따라 View 자체가 어떻게 그려질지 보여준다
  • 이처럼, SwiftUI에서는 수식어의 순서에 따라 오류가 날수도, 보여지는 뷰가 다를수도 있음을 유의하자

 

Tabview

  • Tab을 만들어 각각의 view 마다 다른 화면을 보여주는것
Tabview {
	View()
		.tabItem {
		//- tabItem 의 내용 이미지, 이름 등을 설정
	}
}
  • tabItem에는 Label , Text, Image 만 허용하고, Button 과 같은 컴포넌트가 포함되면 해당 뷰는 빈 페이지로 로드

Data

  • Model 개념의 구조체로 데이터의 ‘청사진' 을 구성
struct Info {
    let image: String
    let name: String
    let story: String
    let hobbies: [String]
    let foods: [String]
    let colors: [Color]
    let funFacts: [String]
}
  • 해당 데이터의 모델 구성하면, 앱의 전반적인 곳에서 해당 모델을 사용할 수 있음
let information = Info(
    image: "Placeholder",
    name: "My Name",
    story: "A story can be about anything you can dream up. There are no right answers, there is no one else.\\n\\nNeed some inspiration?\\n• 🐶🐱🛶️🎭🎤🎧🎸\\n• 🏄‍♀️🚵‍♀️🚴‍♀️⛵️🥾🏂⛷📚\\n• ✍️🥖☕️🏋️‍♂️🚲🧗‍♀️ ",
    hobbies: ["bicycle", "ticket.fill", "book.fill"],
    foods: ["🥐", "🌮", "🍣"],
    colors: [Color.blue, Color.purple, Color.pink],
    funFacts: [
        "The femur is the longest and largest bone in the human body.",
        "The moon is 238,900 miles away.",
        "Prince’s last name was Nelson.",
        "503 new species were discovered in 2020.",
        "Ice is 9 percent less dense than liquid water.",
        "You can spell every number up to 1,000 without using the letter A.\\n\\n...one, two, three, four...ninety-nine...nine hundred ninety-nine 🧐",
        "A collection of hippos is called a bloat.",
        "White sand beaches are made of parrotfish poop.",
    ]
)

View 의 구성

  • VStack 은 컴포넌트 들을 수직으로 정렬
  • Text 를 사용해서 글을 표시할수 있으며 .font / .fontWeight 등의 옵션으로 글을 커스텀 할수 있음
  • padding 을 사용하여 다른 뷰와의 거리를 띄울수 있음
  • Image 를 사용해서 이미지를 추가 할수 있다
    • .resizable() 을 사용하여 이미지의 크기를 조절 할 수 있다
    • .aspectRatio(contentMode: .fit) 을 사용하여 이미지의 종횡비를 지정할 수 있다.
    • .cornerRadius() 를 사용하면 이미지의 모서리를 둥글게 할수 있다.
Imgae("이미지 내용")
  • ScrollView
    • ScrollView 를 구성 하려면 ScrollView { ... } 로 감싸면 된다 
ScrollView { Text(information.story) .font(.body) .padding() }
  • HStack 은 컴포넌트 들을 수평 으로 정렬
  • ForEach 구문을 사용하여 뷰를 구성할 수 있다.
    • id: \.self 를 통해 고유 값을 설정한다 ( 인덱스의 개념! )
ForEach(information.hobbies, id: \\.self) { hobby in 
	Image(systemName: hobby) 
    	.resizable() 
        .frame(maxWidth: 80, maxHeight: 60) 
}
  • @State 를 사용하면 내용이 바뀔때마다 view 를 새로 그린다
  • Button 클로저 아래에 여러 설정값으로 버튼의 모양을 변경할 수 있다
Button("Show Random Fact") { 
	funFact = information.funFacts.randomElement()! 
    } 
    .padding() 
    .background(Color.cyan) 
    .cornerRadius(20)

 

+ Recent posts