[SwiftUI Beginner Review]

Woozoo·2023년 1월 30일
0

[SwiftUI Review]

목록 보기
1/41
post-custom-banner

Text

Text("Hello, world! This is the Swiftful Thinking Bootcamp. I am really enjoying this course and learning a lot."
.multilineTextAlignment(.leading)

.font 같은 방법으로 텍스트 세부조정 가능!
.multilineTextAlignment는 여러줄일 때 align 어디 맞춰 줄건지 설정하는 거임
.baselineOffset은 baseline 오프셋 주는 거고
요렇게 편집툴에서 텍스트 프로퍼티 조절하는 것처럼 다양하게 커스텀해볼 수 있다!

그리고 .frame을 통해서 뷰의 프레임 크기를 지정해줄 수 있음


근데 만약에 프레임의 크기를 벗어나게 될 정도로 텍스트가 길어질 수 있잖음
고럴 때 .minimumScaleFactor 사용해주면 됨!
몇퍼센트 정도 작게 해줄건지 설정해줄 수 있음

Shape

쉐입들을 바로 표현하는게 가능함

요렇게!

.trim을 사용하면 특정부분만큼 자르기도 가능함
응용하면 로딩 애니메이션 같은 것도 만들 수 있겠죠

Color

칼라는 그 자체로도 이미 View 라서 뷰로 바로 반환하는 것도 가능함!
그리고 칼라 리터럴은 없어진 표현이니까 사용하지 말자!!
커스텀하게 칼라를 주고 싶다면 xcAsset파일에 칼라셋을 추가하고 이름을 지정해준 다음에
불러오면 됨!!
Color("지정한 이름") 으로 가져오면 됨

아 그리고 UIKit에 있는 UIColor들을 가지고 오는 것도 가능함

Gradients

그라디언트는 간단하다
원하는 색상들 배열로 넣어주고 포인트 위치 지정해주면 됨

Icons

SFSymbol 사용가능함!
Image(systemName: "symbol이름") 으로 만들어주면 된다

근데 심볼은 기본적인 이미지랑 다르게 font 설정으로도 크기를 바꿔줄 수 있음

.resizable 모디파이해주면 뷰의 크기에 맞게 이미지가 채워지게 됨
그리고 .aspectRatio(contentMode: .fit) 설정해주면 되고
.aspectRatio를 간단하게 표현하면 .scaledToFill()같은 형태로 축약도 가능함

.clipped는 프레임의 크기에 맞춰서 뷰를 잘라버리는 modifier임

.renderingMode로 렌더링 모드 설정 해줄 수 있다!!

Image()


clipShape로 이미지 클리핑이 가능하다!!

가끔 아이콘이미지를 불러오는데 색깔이 커스텀 안될 경우에는

.renderingMode(.template) 붙여주자!!
그럼 .foregroundColor 수정이 가능함

.frame()

스크린에 추가되는 모든 object는 기본적으로 frame을 가지게 됨!
frame은 사각형의 투명한 공간같은 느낌!!

struct FrameBootcamp: View {
    var body: some View {
        Text("Hello, World!")
            .background(Color.red)
            .frame(height: 100)
            .background(Color.orange)
            .frame(width: 150)
            .background(Color.purple)
            .frame(maxWidth: .infinity)
            .background(Color.pink)
            .frame(height: 400)
            .background(Color.green)
            .frame(maxHeight: .infinity, alignment: .top)
            .background(Color.yellow)
    }
}

이렇게 작성한다면

요렇게 만들 수 있겠죠

레이아웃 구성해보다 프레임이 어디쯤 위치한지 확인해볼때 칼라 넣어서 확인해보기!!

.background() & overlay()

background는 뒤로 그려지고 overlay는 앞으로 그려짐

하나의 뷰를 .background와 .overlay를 이용해서 요렇게 구성해줄 수도 있다!!

VStack(), HStack(), ZStack()

방향에 따라서 쌓아주는 뷰임!!
V는 vertical,
H는 horizontal,
Z는 Z방향으로 (어떻게보면 overlay랑 비슷하죠),

VStack이나 HStack은 기본적으로 살짝의 spacing이 있음
없애주고 싶다면 spacing을 0으로 만들어주면 된다!

ZStack에 대해서 알아두면 좋은점!

같은 뷰를 표현하는데 ZStack을 사용할 수 도 있고,
background를 이용해서 표현할 수 도 있음!!

상황에 따라서
뷰가 간단하면 .background를 이용하는 게 가독성이 좋을 때가 있다!!
뷰가 복잡해진다면 물론 ZStack을 이용하는 게 좋겠죠

.padding()

뷰를 표현할 때 .frame으로 크기를 나타낼 수 있다고 했잖음
근데 이렇게 하드코딩하면 불편한 경우가 종종 생김

Text의 크기에 맞춰서 프레임을 100*100으로 표현했는데
Text의 글자가 늘어나게 되면 프레임 내에서만 그려지게 되니까
다시 frame을 조절해줘야할 경우가 생기게됨

그래서 이런 과정을 간단하게 표현하는게 .padding이다!!

Spacer()

Spacer도 기본적으로 mingLength가 정해져있음
그래서 Spacer(minLength: 0) 으로 없애줄 수 도 있음

init() & enums

init은 뷰의 초기 세팅을 해주는 메소드임

enum은 case로 묶어서 표현하기 좋은 타입!

ForEach()

struct ForEachBootcamp: View {
    
    private struct NamedFont: Identifiable {
        let name: String
        let font: Font
        var id: String { name }
    }

    private let namedFonts: [NamedFont] = [
        NamedFont(name: "Large Title", font: .largeTitle),
        NamedFont(name: "Title", font: .title),
        NamedFont(name: "Headline", font: .headline),
        NamedFont(name: "Body", font: .body),
        NamedFont(name: "Caption", font: .caption)
    ]
    
    var body: some View {
        VStack(spacing: 10) {
            ForEach(namedFonts) { namedFont in
                Text(namedFont.name)
                    .font(namedFont.font)
            }
        }
    }    
}

ForEach의 ()안에 들어오는 데이터는 Identifiable 해야함
지금 같은 경우에는 computed property로 id를 name으로 지정해줬음

ScrollView()

스크롤뷰의 showsIndicators에 bool값을 줘서 인디케이터 보여주고 안보여주고 가능
그리고 스크롤 방향도 init할 때 설정 가능하다

struct ScrollViewBootcamp: View {
    var body: some View {
        ScrollView {
            VStack {
                ForEach(0..<10) { index in
                    ScrollView(.horizontal, showsIndicators: false) {
                        HStack {
                            ForEach(0..<12) { index in
                                RoundedRectangle(cornerRadius: 25)
                                    .fill(.white)
                                    .frame(width: 200, height: 150)
                                    .shadow(radius: 10)
                                    .padding()
                            }                            
                        }
                    }
                    
                }
            }
        }
    }
}

이렇게 ForEach를 중첩해서 UIKit에서 구현하기 복잡했던 뷰를 금방 구현 가능

한가지 알아두면 좋은 건 루핑해서 그리는 뷰가 많을 수 있잖음
그럴 때 Stack앞에 Lazy라는 키워드를 붙이면 화면상에 보이는 것 만큼만 로드 가능함

LazyVStack

LazyVGrid()

grid가 UIKit 콜렉션 뷰다!!

struct GridBootcamp: View {
    let columns: [GridItem] = [
        GridItem(.fixed(50), spacing: nil, alignment: nil)
    ]
    
    var body: some View {
        LazyVGrid(columns: columns) {
            Text("Placeholder")
            Text("Placeholder")
        }
    }
}

grid에는 [GridItem] 타입의 columns를 지정해줘야함

struct GridBootcamp: View {
    let columns: [GridItem] = [
        GridItem(.fixed(50), spacing: nil, alignment: nil),
        GridItem(.fixed(50), spacing: nil, alignment: nil),
        GridItem(.fixed(100), spacing: nil, alignment: nil),
        GridItem(.fixed(50), spacing: nil, alignment: nil),
        GridItem(.fixed(50), spacing: nil, alignment: nil),
    ]
    
    var body: some View {
        LazyVGrid(columns: columns) {
            ForEach(0..<50) { index in
                Rectangle()
                    .frame(height: 50)
            }
        }
    }
}

요렇게 GridItem으로 레이아웃을 정해주고 지정된 레이아웃의 크기나 정렬, spacing 으로 뷰를 표현하게 된다

GridItem의 크기를 .flexible()로 설정하면 알아서 화면에 크기가 맞춰지게 됨

struct GridBootcamp: View {
    let columns: [GridItem] = [
        GridItem(.flexible(), spacing: nil, alignment: nil),
        GridItem(.flexible(), spacing: nil, alignment: nil),
        GridItem(.flexible(), spacing: nil, alignment: nil),
        GridItem(.flexible(), spacing: nil, alignment: nil),
        GridItem(.flexible(), spacing: nil, alignment: nil),
    ]
    
    var body: some View {
        LazyVGrid(columns: columns) {
            ForEach(0..<50) { index in
                Rectangle()
                    .frame(height: 50)
            }
        }
    }
}

size를 하드코딩하지 않고도 크기를 자유자재로 표현이 가능해진다!!

struct GridBootcamp: View {
    let columns: [GridItem] = [
        GridItem(.adaptive(minimum: 50, maximum: 300), spacing: nil, alignment: nil),        
    ]
    
    var body: some View {
        LazyVGrid(columns: columns) {
            ForEach(0..<50) { index in
                Rectangle()
                    .frame(height: 50)
            }
        }
    }
}

.adaptive는 정해준 크기만큼 뷰를 최대한 채워주는 용도
근데 잘 사용 안하게 됨


struct GridBootcamp: View {
    let columns: [GridItem] = [
        GridItem(.flexible(), spacing: nil, alignment: nil),
        GridItem(.flexible(), spacing: nil, alignment: nil),
        GridItem(.flexible(), spacing: nil, alignment: nil),
    ]
    
    var body: some View {
        ScrollView {
            
            Rectangle()
                .fill(.white)
                .frame(height: 400)
            
            LazyVGrid(columns: columns) {
                ForEach(0..<50) { index in
                    Rectangle()
                        .frame(height: 150)
                }
            }
        }
    }
}

프리뷰는 요렇게 표현되게 된다


struct GridBootcamp: View {
    let columns: [GridItem] = [
        GridItem(.flexible(), spacing: 6, alignment: nil),
        GridItem(.flexible(), spacing: 6, alignment: nil),
        GridItem(.flexible(), spacing: 6, alignment: nil),
    ]
    
    var body: some View {
        ScrollView {
            
            Rectangle()
                .fill(.orange)
                .frame(height: 400)
            
            LazyVGrid(
                columns: columns,
                alignment: .center,
                spacing: 6,
                pinnedViews: [.sectionHeaders]) {
                    Section {
                        ForEach(0..<20) { index in
                            Rectangle()
                                .frame(height: 150)
                        }
                    } header: {
                        Text("Section 1")
                            .foregroundColor(.white)
                            .font(.title)
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .background(.blue)
                            .padding()
                    }
                    
                    Section {
                        ForEach(0..<20) { index in
                            Rectangle()
                                .fill(Color.green)
                                .frame(height: 150)
                        }
                    } header: {
                        Text("Section 2")
                            .foregroundColor(.white)
                            .font(.title)
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .background(.red)
                            .padding()
                    }
                }            
        }
    }
}


Grid안에서 Section을 구분해주는 것도 가능함!
이때 pinnedViews에 들어가는 건 헤더나 푸터를 스크롤하더라도 띄워줄 수가 있음


요렇게!


ignore the Safe Area

real content는 safeArea에 맞춰서 보여져야 하는데 background같은 경우는 꽉채워줘도 될 때 사용함
.ignoresSafeArea() 요렇게

struct SafeAreaBootcamp: View {
    var body: some View {
        ScrollView {
            Text("Title goes here")
                .font(.largeTitle)
                .frame(maxWidth: .infinity, alignment: .leading)
                .ignoresSafeArea()
            ForEach(0..<10) { index in
                RoundedRectangle(cornerRadius: 25.0)
                    .fill(.white)
                    .frame(height: 150)
                    .shadow(radius: 10)
                    .padding(20)
            }
        }
        .background(
            Color.green                
        )        
    }
}

근데 스크롤뷰 사용할거라면 따로 안해줘도 back이 알아서 safeArea넘겨줌


Button()

Button(action: () -> Void, label: () -> View)

이렇게 action클로져와 label클로져로 보통 구성해주게 됨

action에는 로직, label에는 그려질 뷰 넣어주면 된다


@State

@State로 프로퍼티를 감싸주게 되면 이 프로퍼티의 값이 변경되면 자동으로 UI도 업데이트 해주겠다~ 는 뜻

그러니까 뷰가 프로퍼티의 State 상태를 지켜보겠다~ 는 말


Extract Functions & Views

struct ExtractedFunctionBootcamp: View {
    
    @State var backgroundColor: Color = Color.green
    
    var body: some View {
        ZStack {
            //background
            backgroundColor
                .ignoresSafeArea()
            
            //content
            contentLayer
        }
    }
    
    var contentLayer: some View {
        VStack {
            Text("Title")
                .font(.largeTitle)
            Button {
                buttonPressed()
            } label: {
                Text("Press me")
                    .font(.headline)
                    .foregroundColor(.white)
                    .padding()
                    .background(.black)
                    .cornerRadius(10)
            }
        }
    }
    
    func buttonPressed() {
        backgroundColor = .yellow
    }
}

변수로 빼서 view를 구성하면 body가 짧아지면서 가독성이 좋아짐
extension으로 빼서 구성해주면 더 읽기 좋다!!

Extract Subviews

앞에서 했던 var contentLayer: some View 로 나누는건 여러개의 뷰를 표현하기가 불편하다

그래서 structure로 아예 빼버리는 방법이 있음

struct MyItem: View {
    let title: String
    let count: Int
    let color: Color
    var body: some View {
        VStack {
            Text("\(count)")
            Text(title)
        }
        .padding()
        .background(color)
        .cornerRadius(10)
    }
}

struct ExtractSubviewBootcamp: View {
    var body: some View {
        ZStack {
            Color(.systemTeal).ignoresSafeArea()
            
           contentLayer
        }
    }
    
    var contentLayer: some View {
        HStack {
            MyItem(title: "Apples", count: 1, color: .red)
            MyItem(title: "Oranges", count: 10, color: .orange)
            MyItem(title: "Bananas", count: 34, color: .yellow)
        }
    }
}

요렇게!


@Binding

부모뷰랑 child View가 있다고 해봅시다

부모뷰에 선언된 프로퍼티를 child View는 알 수가 없음
프로퍼티의 스코프는 선언된 곳에 한정되니까

새로 변수를 파서 값을 전달해줘도 되겠지만 그럼 여러개의 상태들을 계속해서 만들어주고 값도 전달해줘야해요

이럴 때 @Binding 프로퍼티 래퍼를 사용하면 부모뷰가 가진 [@State로 선언된] 프로퍼티를
말 그대로 바인딩 해줄 수 있습니다.

그리고 부모뷰에서 자식뷰를 호출하게 되었을 때 $ 사인을 붙여서 값을 넘겨주면 됩니다

struct BindingBootcamp: View {
    
    @State var backgroundColor: Color = .green
    @State var title: String = "Title"
    
    var body: some View {
        ZStack {
            backgroundColor
                .ignoresSafeArea()
            VStack {
                Text(title)
                    .foregroundColor(.white)
                ButtonView(backgroundColor: $backgroundColor, title: $title)
            }
        }
    }
}

struct ButtonView: View {
    @Binding var backgroundColor: Color
    @State var buttonColor: Color = .blue
    @Binding var title: String
    var body: some View {
        Button {
            backgroundColor = .orange
            buttonColor = .pink
            title = "New Title!"
        } label: {
            Text("Button")
                .foregroundColor(.white)
                .padding()
                .padding(.horizontal)
                .background(buttonColor)
                .cornerRadius(10)
        }
    }
}

Adding Animations

애니메이션을 추가하고 싶다면 ui를 업데이트 시키는 값에 withAnimation으로 감싸주면 됨

struct AnimationBootcamp: View {
    @State var isAnimated: Bool = false
    var body: some View {
        VStack {
            Spacer()
            Button("Button") {
                withAnimation {
                    isAnimated.toggle()
                }
            }
            Spacer()
            RoundedRectangle(cornerRadius: isAnimated ? 50:25)
                .fill(isAnimated ? .blue : .green)
                .frame(
                    width: isAnimated ? 100 : 300,
                    height:isAnimated ? 100 : 300)
                .rotationEffect(Angle(degrees: isAnimated ? 360 : 0))
                .offset(y: isAnimated ? 200 : 0)                
            Spacer()
        }
    }
}


() 안에서 Animation의 세부 설정들을 줄 수 있음


이렇게 뷰 자체에 .animation 을 이용해서 애니메이션도 가능하다
이 때 value는 변하게되는 값을 넣어주면 됨

withAnimation을 사용하는게 일반적임!

Animation Curves

struct AnimationTimingBootcamp: View {
    @State var isAnimating: Bool = false
    let timing: Double = 10
    var body: some View {
        VStack {
            Button("Button") {
                isAnimating.toggle()
            }
            RoundedRectangle(cornerRadius: 20)
                .frame(width: isAnimating ? 300 : 50, height: 100)
                .animation(Animation.spring(), value: isAnimating)
            RoundedRectangle(cornerRadius: 20)
                .frame(width: isAnimating ? 300 : 50, height: 100)
                .animation(Animation.easeIn(duration: timing), value: isAnimating)
            RoundedRectangle(cornerRadius: 20)
                .frame(width: isAnimating ? 300 : 50, height: 100)
                .animation(Animation.easeInOut(duration: timing), value: isAnimating)
            RoundedRectangle(cornerRadius: 20)
                .frame(width: isAnimating ? 300 : 50, height: 100)
                .animation(Animation.easeOut(duration: timing), value: isAnimating)
        }
    }
}

이징커브 그래프 (duration: ) 에 Double 값을 줘서 타이밍 조절 가능
커브 종류마다 추가로 설정가능한 항목들도 있다!

.transition()

struct TransitionBootcamp: View {
    
    @State var showView: Bool = false
    var body: some View {
        ZStack(alignment: .bottom) {
            VStack {
                Button("Button") {
                    withAnimation {
                        showView.toggle()
                    }
                }
                Spacer()
            }
            
            if showView {
                RoundedRectangle(cornerRadius: 30)
                    .frame(height: 500)
                    .transition(.slide)                    
            }
        }
        .background()
        .edgesIgnoringSafeArea(.bottom)
    }
}

트랜지션 되길 원하는 뷰에 .transition으로 트랜지션 추가할 수 있다

근데 캔버스에서는 제대로 작동안해서 시뮬레이터 꼭 돌려봐야함!!


그리고 트랜지션에 애니메이션 그래프 추가도 가능하다!


시작되는 트랜지션과 끝나는 트랜지션을 나눠서 줄 수도 있다!!


Display pop-up sheets and fullscreencovers

모달로 새로운 뷰를 띄워주고 싶을 경우에 .sheet을 사용할 수 있음

'바인딩'된 Bool값에 따라서 sheet이 동작하게 된다

새로운 뷰에선 dismiss Action을 @Environment를 이용해서 추가해줄 수 있는데

예전에는 @Environment(\.presentationMode) var presentationMode 로 사용했다면
지금은 @Environment(\.dismiss) var dismiss와 같은 형태로 선언해주면 됨

struct SecondScreen: View {
    @Environment(\.dismiss) var dismissScreen
//    @Environment(\.presentationMode) var presentationMode
    var body: some View {
        ZStack(alignment: .topLeading) {
            Color.purple
                .ignoresSafeArea()
            
            Button {
                dismissScreen()
            } label: {
                Image(systemName: "xmark")
                    .foregroundColor(.white)
                    .font(.largeTitle)
                    .padding(10)
            }
        }
    }
}

그리고 하나의 뷰에는 하나의 sheet만 넣어줘야하고
sheet의 content에는 conditional 로직이 들어가면 안된다!!
if else 쓰면 안됨

풀스크린으로 띄워줄 수도 있음

.sheet() vs .transition() vs .animation()

sheet으로도 트랜지션으로도 애니메이션으로도 같은 효과를 보여줄 수가 있다!
상황에 따라서 편한걸 쓰면됨

이렇게 sheet으로도 구성해줄 수 있을 것이고


이렇게 .transition을 추가해줄수도 있을텐데 현재 .animation의 밸류가 없는 모디파이어는 사라질 표현이라서 안쓰는게 좋을 거 같다


지금처럼 offset을 활용해서 animation을 줄수도 있음!
근데 이것도 .animation의 밸류를 넣지 않은 모디파이어는 사라질 표현이라서 사용하지 않도록 하자

결론은 .sheet() 씁시다..!
(특별하게 트랜지션을 만들어줘야하는 상황이 아니라면)


네비게이션뷰는 여러개를 중첩하면 안됨!!
앱의 최상단에만 하나 넣어주면 됩니다

상단에 NavigationView를 넣어주면 네비게이션 타이틀을 지정해줄 수 가 있게된다!
그리고 이 타이틀을 없애주고 싶다면 .toolbar(.hidden)을 적용해주면 됨

NavigationLink로 감싸준 뷰들은 버튼처럼 바뀌게 됨

NavigationLink를 구성할 때 destination이 되는 뷰와 label 뷰가 필요하게 되는데
텍스트로만 이루어진 간단한 뷰로 Navigation을 만들고 싶다면 아래에 있는 것처럼 쉽게 표현이 가능하다

디스미스 액션도 @Environment로 추가 가능
(이렇게 할 경우엔 toolbar를 숨기고 싶어서 back button이 사라졌을 때임)

.toolbar 모디파이어를 이용해서 ToolbarItem을 추가할 수 있는데 이때 들어오는 아이템을
Button으로 만들수도 있겠고, NavigationLink로 구성해줄 수도 있음


색을 바꿔주고 싶다면 .tint로 적용해주자


Add, edit, move, and delete items in a List

List를 활용하면 리스트뷰를 표현할 수 있다.
리스트의 내용은 ForEach뷰를 사용해서 반복하면 간편하게 표현가능


ForEach안에 반복하게 될 대상인 RandomCollection이 오게 되고, 이 각각의 아이템들은 identifiable해야함. 지금은 따로 Model을 만들어준 게 아니라 array를 반복하는 거니까 id: \.self 를 붙여줬다
그리고 트레일링 클로져로 fruits의 각각의 개체들이 될 fruit 을 가지고 Text뷰로 루핑해줌

리스트엔 섹션을 추가해줄 수 있는데 해당 섹션의 헤더나 푸터를 구성해줄수 있음


이렇게!

delete와 move 구현하기

.onDelete를 이용해서 delete로직을 구성해줄 수 있음

클로져 형태로 바꾸게 될 경우에 IndexSet을 받는걸 볼 수 있음
IndexSet이란


해당 콜렉션의 Index들의 모음과도 같음

fruits 배열에 .remove 입력하면 at으로 받는 파라미터랑 atOffsets로 받는 파라미터 두개가 나오는데 IndexSet 타입을 받는 건 아래의 atOffsets임!!
이렇게 구현하면 delete가 선택된 아이템의 index를 알아차리고 해당 index를 삭제하는 게 가능하다


delete 메소드를 따로 구성해주고 .onDelete에 넣어줌
근데 이거 더 간단하게 표현도 가능하다

.onDelete(perform: delete) 처럼 구성해줄 수 있음
괄호가 생략가능한 건 어떤 타입을 받는지가 서로 동일하기 때문!!

툴바아이템에 EditButton을 추가하고나면 .onMove도 활용할 수 있게됨

클로져 파라미터로 IndexSet, Int가 필요한 걸 볼 수 있다

이것도 따로 메소드로 빼주면 가독성이 좋아지겠죠

func onMove(indexSet: IndexSet, newOffset: Int) {
	fruits.move(fromOffsets: indexSet, toOffset: newOffset)
}

저번에도 겪었던 현상인데 list의 아이템을 꾹 누르면 드래그 드롭이 가능해짐. 캔버스에선 제대로 동작하지 않고 시뮬레이터에서만 작동한다! 이 기능 끌 수 없으려나...🤔


트레일링 네비게이션아이템에 "Coconut"을 추가해주는 버튼을 구성했다


add로직도 빼고, addButton도 새로 구성해줌


.listStyle도 커스텀 가능하다

tint 값을 리스트에 줘서 툴바아이템 색 변경이 가능하고
섹션의 헤더 같은 경우엔 foregroundColor를 변경해주면 칼라 변경가능함

.listRowBackground 같은 모디파이어도 있고
다양하게 커스텀가능


.alert()

struct AlertBootcamp: View {
    @State var showAlert: Bool = false
    
    var body: some View {
        Button("Click here") {
            showAlert.toggle()
        }
        .alert(isPresented: $showAlert) {
            Alert(title: Text("There was an error!"))
        }
    }
}

예전 버전의 alert은 이렇게 구현했음

body내에 .alert로 Alert를 구성해주면 됐다


Alert를 상세하게 지정해줄수도 있음


메소드로 빼주게 되면 body좀더 깔끔하게 볼 수 있다!

enum으로 반환되는 Alert를 관리해줄 수도 있음


옵셔널로 enum을 선언하고


error냐 success냐에 따라서

지정해주면 보기 간편하다


현재 Xcode에서 구현해줘야하는 alert 방식을 이렇게 바뀜



.actionSheet

alert이랑 거의 비슷함


이 표현도 사라질 예정

.confirmatonDialog를 사용하면 됨


.contextMenu


.contextMenu 붙여주면 됨
꾹 눌렀을 때 나오는 거 말하는 겨


TextField()


텍스트 필드는 placeholder랑 binding된 스트링을 전달해주면 됨


요렇게!


스타일도 설정해 줄 수 있다

이전에 ui만들어줄 때 하던 작업들과 비슷하게 modifier도 적용 가능

TextEditor()

입력해야할 텍스트가 여러줄이 되면 TextEditor 사용하면 됨

근데 TextEditor는 .background가 안먹혀서 .colorMultiply사용해서 백그라운드 바꿔줄 수 있음

Toggle()

struct ToggleBootcamp: View {
    
    @State var toggleIsOn: Bool = false
    
    var body: some View {
        VStack {
            HStack {
                Text("Status:")
                Text(toggleIsOn ? "Online":"Offline")
            }
            .font(.largeTitle)
            
            Toggle(isOn: $toggleIsOn) {
                Text("Change status")
            }
            .toggleStyle(.automatic)
            .tint(.orange)
            
            Spacer()
        }
        .padding([.horizontal, .top], 30 )
    }
}

Toggle 자체는 버튼기능만 있음
단순히 Bool값을 토글해주기만한다


Picker()


Content가 되는 Text("1") 자체는 보여지는 부분을 말하는 거고,
.tag는 해당 뷰가 선택될때 제공되게 될 밸류를 지정해줌

Picker의 selection 파라미터는 Binding된 값이 들어오게 되는데 선택된 tag값이 바인딩되게 된다
그러니까 뭐가 선택 되었니? 라는 물음에 tag가 답해주고 selection에 전달되게됨
처음에 피커가 로드 될 땐 해당 selection의 값부터 시작하겠죠

마지막에 붙는 label 파라미터는 아무짝에도 쓸모가 없음
voiceover용이라고 하기에도 뭐한게 애초에 저 label이 필요하지가 않은데 왜 있는지 모르겠다 아직 label파라미터를 활용할 방법을 못찾아서 그런걸까?

ColorPicker

가 있긴 한데 잘 안쓸 것 같다

struct ColorPickerBootcamp: View {
    
    @State var backgroundColor: Color = .green
    var body: some View {
        ZStack {
            backgroundColor
                .ignoresSafeArea()
            
            ColorPicker("Select a color", selection: $backgroundColor, supportsOpacity: true)
                .padding()
                .background().cornerRadius(10)
                .font(.headline)
                .padding(50)
        }
    }
}

DatePicker()

따로 클로져도 필요하지 않다!
이게 선언형의 힘인가!!


작성된 코드는 시뮬레이터에선 이렇게 나온다
.tint로 틴트값도 변경 가능 (Picker를 선택하게 되었을 때 표시되는 ui들의 tint가 변경되게 됨)


datePicker스타일도 설정해줄 수 있는데 몇몇은 iOS말고 macOS에서 표시되는 스타일임


파라미터로 display되는 컴포넌트 설정을 해줄 수가 있는데


표시하고 싶은 항목을 배열형태로 구성해주면 됨

원하는 날짜 내에서 선택가능하게 해줄수도 있다

두개의 날짜 범위를 정해주고 in 파라미터에 전달해주면 됨

date를 쓰게 될 때 제일 많이 사용하게 될 DateFormatter를 구성할 수 있다


formatter를 computed Property로 만들어주고


바꾸고 싶은 date를 스트링으로 변환시켜주면 됨


Stepper()

Stepper에 들어가게 되는 value는 Strideable해야하는데 basically go up and down 해야함
쉽게 Int같은 범위 라고 생각하면 되겠다

struct StepperBootcamp: View {
    
    @State var stepperValue: Int = 10
    
    var body: some View {
        Stepper("Stepper: \(stepperValue)", value: $stepperValue)
            .padding(50)
    }
}

그리고 Stepper는 Increment일 때 로직이랑 Decrement일 때 로직 나눠줄 수도 있음

Slider()

struct SliderBootcamp: View {
    
    @State var sliderValue: Double = 3
    @State var color: Color = .green
    
    var body: some View {
        VStack {
            Text("Rating:")
            Text(String(format: "%.0f", sliderValue))
                .foregroundColor(color)
//            Slider(value: $sliderValue)
//            Slider(value: $sliderValue, in: 1...5)
            Slider(value: $sliderValue, in: 1...5, step: 1.0)
                .tint(.red)
        }
    }    
}

slider는 값이 변할 밸류랑 변하게될 닫힌 범위, step 정도 넣어주면 됨

TabView() & PageTabViewStyle()

struct TabViewBootcamp: View {
    
    var body: some View {
        TabView {
            Text("Home Tab")
                .tabItem {
                    Image(systemName: "house.fill")
                    Text("Home")
                }
            
            Text("Browse Tab")
                .tabItem {
                    Image(systemName: "globe")
                    Text("Browse")
                }
            
            Text("Profile Tab")
                .tabItem {
                    Image(systemName: "person.fill")
                    Text("Profile")
                }
        }        
    }
}

탭뷰는 탭뷰안에 넣고 싶은 뷰 넣고, .tabItem 붙여주면 끝

selection 프로퍼티 추가하면 처음 탭이 어떤 건지 표시해줄 수 있음


TabView의 다른 형태인 PagingTab


.tabViewStyle만 바꿔주면
UIKit에서 구현하기 귀찮았던 이 뷰를 손쉽게 구현 가능


Darkmode

@Environment(.colorScheme) var colorScheme
프로퍼티가 있는데 darkmode이면 false 값을 가짐


.onAppear & .onDisappear

onAppear와 onDisappear는 말그대로 뜰 때랑 안뜰 때에 로직을 구성해줄 수 있음


.onTapGesture

.onTapGesture로 추가해줄 수 있다.
버튼이랑 다른 점이 있다면 눌렸을 때 하이라이트 되면서 틴트값이 바뀌지는 않음
몇번 눌렸을 때 gesture를 호출할 건지 설정도 가능하다


@StateObject & @ObservableObject


@EnvironmentObject

environment는 정말 필요한 애들만 추가하는 게 좋다


@AppStorage

@AppStorage("name") var currentUserName: String?
과 같은 형태로 추가할 수 있다
구조체 내부에서 사용할 땐 @AppStorage가 UserDefaults보다 나음

UserDefaults로도 물론 저장해줄 수 있음

UserDefaults를 set 해주고
원하는 부분에서 string key로 찾아주면 됨

AsyncImage

iOS 15에 추가된 기능

https://picsum.photos/200
picsum url을 써서 이미지 가져와봅시다

AsyncImage를 쓸 때 문제는 .frame으로 프레임크기를 지정해도 원래 다운받은 Image의 크기로 이미지가 나온다는 거

struct AsyncImageBootcamp: View {
    
    let url = URL(string: "https://picsum.photos/400")
    
    var body: some View {
        AsyncImage(url: url, content: { returnedImage in
            returnedImage
                .resizable()
                .scaledToFit()
                .frame(width: 100, height: 100)
        }, placeholder: {
            ProgressView()
        })
    }
}

content 클로져가 있는 init으로 만들어주면 returnedImage가 Image 뷰 타입이라 .resizable()이랑 .frame으로 이미지 크기 변경이 가능해진다

AsyncImage initializer 중에 phase를 받는 파라미터가 있는데 문서를 읽어보면 로딩을 하는 phase에 대한 처리가 가능함

struct AsyncImageBootcamp: View {
    
    let url = URL(string: "https://picsum.photos/400")
    
    var body: some View {
        
        AsyncImage(url: url) { phase in
            switch phase {
            case .empty:
                ProgressView()
            case .success(let image):
                image
                    .resizable()
                    .scaledToFit()
                    .frame(width: 100, height: 100)
                    .cornerRadius(20)
            case .failure:
                Image(systemName: "questionmark")
                    .font(.headline)
            default:
                Image(systemName: "questionmark")
                    .font(.headline)
            }
        }
    }
}

요런식으로! phase에 대한 case들을 구분지어서 처리가 가능합니다~!


Background Materials

.background에서 .material로 접근해서 투명한 백 만들 수 있음


TextSelection()


텍스트 카피 가능하게 해주는거!

profile
우주형
post-custom-banner

0개의 댓글