Udemy(유데미) 클론코딩 - 인스타그램

정승호·2022년 6월 21일
0

시작하기에 앞서

본 포스팅은 Udemy 클론코딩 강의를 보고 작성하였습니다.
Instagram SwiftUI Clone | MVVM | Cloud Firestore

강의는 1 ~ 15강 까지를 정리하였고, MainTabView 와
Feed, Search, UploadPost, Notification, Profile
인스타그램 5개 화면을 구성하는 내용을 담고 있습니다.

포스팅 내용은 이렇습니다.

  1. 강의에서 사용된 SwiftUI 문법들을 따로 정리하여 설명합니다.
  2. 코드 구조를 자세하게 분석하여 설명, 이해를 돕습니다.

SwiftUI 파일 생성

화면을 구성하는 과정에서 미완성 뷰를 호출하는 작업이 많습니다.
하여 필요한 파일들을 먼저 생성하고 진행토록 하겠습니다.

Xcode에서 command + n 을 눌러 아래 SwiftUI파일들을 생성합니다.

Main - MainTabView

Feed - FeedView, FeedCell

Items - PostGridView, UserCell, UserListView

Search - SearchView, SearchBar

Notification - NotificationView, NotificationCell

Profile - ProfileView, ProfileHeaderView, UserStatView, ProfileActionButtonView

UploadPost - UploadPostView


MainTabView

MainTabView화면이 완성된 모습입니다.

TabView기능을 사용하여 인스타그램 하단의 탭바를 구현하겠습니다.

TabView란?

  • 사용자가 화면 하단 탭바를 사용하여 여러 View를 전환할 수 있는 기능

TabView 구현 순서

  1. TabView에서 호출할 SwiftUI 파일 생성
  1. SwiftUI 파일을 TabView에서 호출
TabView {
	FeedView()
}
- 이미지가 없는 상태의 탭바가 생성됩니다.

  1. .tabItem를 사용, 안에 Image "house"를 지정합니다.
TabView {
	FeedView()
    	.tabItem {
   			Image(systemName: "house")
  	 }
}

Image 아래 문자열 표기

Text, label 둘 중 편한 것을 사용하면 됩니다.

  • Text 사용
TabView {
	FeedView()
    	.tabItem {
        	Text("Feed")
        	Image(systemName: "house")	
      }
}
  • label 사용
TabView {
	FeedView()
    	.tabItem {
        	Label("Feed", systemImage: "house")	
      }
}

TabView가 무엇인지, 어떻게 사용하는지 알아보았습니다.
본격적으로 인스타그램의 탭뷰를 구현해보겠습니다.

MainTabView내 TabView사용

  1. TabView에 필요한 Swift 파일들을 TabView내에서 호출합니다.
  1. tabItem 내 Image를 지정하면 집, 돋보기와 같은 이미지가 탭바 안에 표시됩니다.
  1. navigationTitle, navigationBarTitleDisplayMode, accentColor 를 사용합니다.
NavigationView {
            TabView {
                FeedView()
                    .tabItem {
                        Image(systemName: "house")
                    }
                SearchView()
                    .tabItem {
                        Image(systemName: "magnifyingglass")
                    }
                
                UploadPostView()
                    .tabItem {
                        Image(systemName: "plus.square")
                    }
                
                NotificationView()
                    .tabItem {
                        Image(systemName: "heart")
                    }
                
                ProfileView()
                    .tabItem {
                        Image(systemName: "person")
                }
            }
            .navigationTitle("Home")
            .navigationBarTitleDisplayMode(.inline)
            .accentColor(.black)
        }
  • navigationTitle에 원하는 문자열을 넣고,
    navigationBarTitleDisplayMode(.inline)을 사용하면
    아래 사진의 Home이 네비게이션바(앱바) 안에 들어갑니다.

  • accentColor를 지정하지 않으면 아래 사진의 파란색으로 기본 설정 됩니다.

  • Xcode 인스펙터에서도 3가지 모두 변경 가능합니다.


Feed

Feed화면이 완성된 모습입니다.

Feed화면은 FeedCell, FeedView파일에서 코드를 작성하여 구현합니다.

FeedCell

FeedCell이 완성된 모습입니다.

FeedCell은 밑의 각 기능들의 설명을 보고 따라만드시면 됩니다.

V(H,Z)Stack

VStack : 수직(Vertical)으로 View를 쌓는다.
HStack : 수평(Horizontal)으로 View를 쌓는다.
ZStack : 동일한 수평과 수직 값을 갖지만 View를 겹쳐서 쌓는다.

FeedCell화면은 상단부터 아래로
계정사진과이름, 포스팅 사진, 좋아요버튼, 좋아요 갯수, 포스팅 글, 날짜 순으로 나와있습니다.

이 순서는 수직형태로 이루어져 있습니다.

이렇게 수직으로 이미지, 버튼, 등을 쌓을 때는 VStack 을 사용합니다.

계정사진 우측엔 donkey라는 계정명이, 좋아요 하트버튼 우측으론 댓글 버튼과 DM버튼이 보입니다.

만약 VStack에 이 버튼을 넣는다면 하트버튼 밑으로 생성 됩니다.

VStack내에 HStack을 넣어 수평으로 생성될 수 있도록 합니다.

이해를 돕기위한 예시

밑의 코드와 함께 보면 됩니다.
VStack(alignment: .leading) {
            HStack {
                Image("udong")
                    .resizable()
                    .scaledToFill()
                    .frame(width: 36, height: 36)
                    .clipped()
                    .cornerRadius(18)
                
                Text("donkey")
                    .font(.system(size: 14, weight: .semibold))
            
            }
            .padding([.leading, .bottom], 8)
            
                Image("tiramisu")
                    .resizable()
                    .scaledToFill()
                    .frame(maxHeight: 300 )
                    .clipped()
	}

VStack안에 있는 tiramisu이미지는 udong이미지 밑으로 생성 되었습니다.
HStack안에 있는 donkey텍스트는 udong이미지 우측으로 생성 되었습니다.

alignment

정렬은 .leading .center .trailing 이 있습니다.

위 사진에서 계정사진은 왼쪽에 위치합니다.
이는 .leading 정렬을 해주었기 때문입니다.

사용방법
VStack (alignment: .leading)
정렬을 해주지 않는다면 밑의 사진과 같은 모습이 됩니다.

Image

이미지는 쓰임새에 맞게 다양한 방법으로 조정, 조절 할 수 있습니다.

이미지를 아래와 같이 원형으로 만들 때 필요한 속성들 입니다.

 HStack {
                Image("tiramisu")
                    .resizable()
                    .scaledToFill()
                    .frame(width: 36, height: 36)
                    .clipped()
                    .cornerRadius(18)
                
                Text("mangwontira")
                    .font(.system(size: 14, weight: .semibold))
            }

resizable : 크기 자동조정

frame : 프레임

width : 폭, 너비 / height : 높이

clipped : 프레임을 벗어나는 이미지 제거

cornerRadius : 코너반경

scaledToFill : 프레임 바깥으로 이미지가 넘어가도 상관없습니다.

scaledToFit : 프레임 안쪽으로 이미지가 조정됩니다.


Text

텍스트 사용방법은 아래 코드와 같습니다.

  Text("donkey")
                    .font(.system(size: 14, weight: .semibold))

인스펙터에서 Text의 다양한 속성을 변경할 수 있습니다.

Button

현재 버튼의 액션은 없지만 만들어 두고 눌러봅니다.

		HStack(spacing: 16) {
                Button(action: {}, label: {
                    Image(systemName: "heart")
                        .resizable()
                        .scaledToFill()
                        .frame(width: 20, height: 20)
                        .font(.system(size: 20))
                        .padding(4)
                })
                Button(action: {}, label: {
                    Image(systemName: "bubble.right")
                        .resizable()
                        .scaledToFill()
                        .frame(width: 20, height: 20)
                        .font(.system(size: 20))
                        .padding(4)
                })
                Button(action: {}, label: {
                    Image(systemName: "paperplane")
                        .resizable()
                        .scaledToFill()
                        .frame(width: 20, height: 20)
                        .font(.system(size: 20))
                        .padding(4)
                })
            }

FeedView

  1. FeedView 에서는 ScrollView, LazyVStack, ForEach를 사용합니다.
  2. 만들었던 FeedCell을 밑의 코드와 같이 ForEach함수 내에서 호출하면 Feed화면이 완성됩니다.

ScrollView

스크롤 가능한 콘텐츠 영역 내에 콘텐츠를 표시합니다.
가로, 세로 또는 둘 다 스크롤할 수 있지만 확대/축소 기능은 제공하지 않습니다.

LazyVStack

LazyVStack은 VStack과 스크롤바 범위 차이가 있습니다.

VStack일때는 텍스트가 끝나는지 점에 스크롤바가 생성됩니다.
LazyVStack은 화면의 넓이에 맞게 스크롤바가 생성됩니다.

LazyVStack은 스크롤할 때마다 데이터를 로드해 주지만
VStack은 데이터를 모두 다 추가합니다.
최적화 차이가 있다는 말과 같습니다.

ForEach

forEach는 반복하고 싶은 구문(FeedCell())을
forEach라는 함수의 매개변수(Parameter) "클로저"로 작성해서 넘겨주는 것 입니다.

 ForEach(0 ..< 10) { _ in
            FeedCell()
     		}

** 함수의 괄호() 안에 들어가는 것을 매개변수(Parameter) 라고합니다.

for - in 반복문과 forEach함수의 차이

for - in

func printForIn() {
    let nums = [1, 2, 3]
    
    for num in nums {
        print(num)
        return
    }
}

for - in의 경우 반복문을 돌다가 return을 만나면 함수 자체가 종료됩니다.
따라서 위 함수를 실행할 경우, 프린트 값은 1

forEach

func printForEach() {
    let nums = [1, 2, 3]
    
    nums.forEach {
        print($0)
        return
    }
}

forEach는 내가 전달한 클로저를 요소 갯수 만큼 실행 하기 때문에
반복 횟수에 영향을 주지 않습니다. 프린트 값은 1, 2, 3

Items

PostGridView

PostGridView는 Search, Profile화면에 쓰입니다.

LazyVGrid ForEach NavigationLink 의 개념과 사용방법을
아래 코드와 함께 보며 설명하겠습니다.

  1. GirdItem이 있는 items상수를 선언합니다.
  2. 디바이스의 너비값의 1 / 3인 width상수를 선언합니다.
  3. LazyVGrid를 만들고 columns값엔 items를, spacing으로 수직 간격을 조절합니다.
  4. content 안에는 ForEach로 목적지가 FeedView, 이미지가 있는 NavigarionLink를 순차적으로 나타내줍니다.

접근제어자 private

private 사용시 같은타입 안에서만 사용가능하며 그외에 어떠한 상황에서도 접근 불가능 합니다.

UIScreen.main.bounds.width

UIScreen.main.bounds.size.width 를 이용하면 디바이스의 너비값을 알 수 있습니다.

LazyV(H)Grid

SwiftUI 에서 Grid 뷰를 그릴 때 LazyVGrid 또는 LazyHGrid 를 사용합니다.

LazyVGrid : 위아래로 스크롤
LazyHGrid : 좌우로 스크롤

LazyVGrid를 만들땐 GridItem이 반드시 필요합니다.

  • List와 Grid의 차이

목적지 뷰를 설정하여 누르면 넘어갈 수 있게합니다.

  • destination 목적지

UserCell

Search화면에서 TextField를 누르면 나오는 Usercell 입니다.

코드와 함께 보며 만들어 보겠습니다.

		HStack {
            Image("gogi")
                .resizable()
                .scaledToFill()
                .frame(width: 48, height: 48)
                .clipShape(Circle())
            
       		 VStack(alignment: .leading) {
                Text("B.B")
                    .font(.system(size: 14, weight: .semibold))
                
                Text("K.S")
                    .font(.system(size: 14))
               
                
            }
            Spacer()
        }
  1. HStack 안에 이미지를 넣습니다.
  2. HStack 안에 Vstack을 넣고 2개의 Text를 만듭니다.
  3. HStack 안에 Spacer를 넣어 좌우 간격을 벌립니다.

Spacer

이미지와 이미지 사이에 Spacer() 를 써주면 간격을 최대한 띄웁니다.

UserListView

ScrollView LazyVStack ForEach NavigationLink 를 사용하고,
UserCell을 호출 하여 완성합니다.

코드와 함께 보며, 이 전까지 다루었던 내용들을 참고하여 만들면 됩니다.

Search

TextField를 누르기 전, 후 화면 모습입니다.

기능 설명

  • 화면 상단엔 SearchBar가 고정되어 있고,
    TextField 탭 여부에 따라 PostGridView, UserListView가 각각 나타나게 만듭니다.
  • Cancel버튼을 만들어 UserListView에서 활성된Cancel버튼을 누르게 되면 다시 PostGridView가 보이는 화면으로 오게 만듭니다.

SearchView

SearchBar

Search 화면은 SearchView와 SearchBar파일 화면을 수시로 전환하며 만듭니다.
하여 두가지 파일을 번갈아 가며 설명하는 부분이 많습니다.

만드는 순서

  • 1, 2번은 SearchView에서 필요한 파일을 호출하고,
  • 3, 4번은 @State, @Binding을 사용하여 TextField구현에 관한 내용입니다.
  • 5 ~ 11번은 TextField를 탭하면 UserListView를 보여주고 Cancel버튼이 생깁니다.
    Cancel버튼을 누르면 PostGridView를 보여줍니다.
  1. SearchView
    ScrollView내 SearchBar를 호출합니다.
  2. SearchView
    ZStack 내 UserListView, PostGridView를 호출합니다.
1, 2번의 이해를 돕기위한 코드
ScrollView {
            SearchBar()
                .padding()
            ZStack {
                    UserListView() // 
                    PostGridView() // 
                }
            }
        
  1. SearchBar
    1) HStack내 TextField를 만듭니다.
    2) TextField의 text의 자료형을 $name으로 지정합니다.
    3) @Binding var name: String 를 선언합니다.

$name으로 지정, name변수는 String으로 @Binding 선언되었기때문에
TextField에 입력을 할 때 마다 바인딩 변수로 값이 이동합니다.

@Binding은 부모의 뷰에서 값을 가져오고 싶을 때 사용하는 키워드 입니다.
그 부모의 뷰는 SearchBar를 호출한 SearchView입니다.

struct SearchBar: View {
    @Binding var name: String
    
    var body: some View {
        HStack {
            TextField("Search...", text: $name)
  1. SearchView
    1) @State var searchText = "" 를 선언합니다.
    2) 호출한 SearchBar 소괄호 내 name: $searchText로 지정합니다.

값이 변경 가능한 @State로 선언합니다.

3번에서 TextField에 입력한 값이 바인딩 변수로 이동,
name: $searchText로 지정해 주었으니
@State var searchText = "" 의 ("")부분을 수정해 주면 Textfield에도 반영이 됩니다.

struct SearchView: View {
    @State var searchText = ""
    var body: some View {
                
        ScrollView {
            SearchBar(name: $searchText)

@State

일반적으로 Struct는 값 타입이라 Struct 안에 있는 값을 변경할 수 없습니다.
그래서 만든 것이 @State 키워드, 이는 struct 내에서 값을 변경가능하도록 도와줍니다.

$

@Binding

자신의 뷰가 아닌 부모의 뷰에서 값을 가져오고 싶을 때 사용하는 키워드입니다.
뷰끼리 양방향으로 연결 해서 서로 값을 공유할 수 있도록 도와주는 키워드 입니다.

  • 바인딩 후 constant

정리

@State는 주가되는 View에 한번 선언합니다.
이 프로퍼티를 다른 뷰에서 사용하기위해 @Binding을 사용합니다.
그리고 사용시에는 $를 앞에 붙여 Binding변수 임을 나타냅니다.
View UI에서 특정 동작으로 해당 프로퍼티 값이 변경되는 자리에는 앞에 $를 붙여줍니다.


.overlay 를 사용하여 돋보기 이미지도 넣어줍니다.

.overlay

  • overlay는 뷰 원본의 공간을 기준으로 그 위에 새로운 뷰를 중첩하여 쌓는 기능을 하는 수식어 입니다.


5 ~ 11번의 내용입니다.

  1. @State var isSearchMode = false 선언합니다.
  2. ZStack 으로 조건문을 작성한 뒤 false, true 값을 바꾸며 PostGridView, UserListView가 나타나는지 확인합니다.
  3. SearchBar에서 @Binding var isEiting: Bool 선언합니다.
  4. SearchView에서 호출한 SearchBar 내에 isEditing: $isSearchMode 지정합니다.
  5. TextField에 onTapGesture를 사용합니다.
    TextField를 탭하면 isEditing = true 가 되어 UserListView가 나타납니
    다.
  6. 조건문 isEditing이 true이면 버튼이 나타납니다.
    이 버튼을 누르면 isEditing = false가되어, PostGridView가 나타납니다.
    label은 Cancel로 지정합니다.
    text = "", 버튼을 누르면 PostGridView가 나타나며 TextField를 빈 값으로 만들어 줍니다.

Notification

알림화면 입니다.

NotificationView

ScrollView LazyVStack ForEach 을 사용하고,
NotificationCell 을 호출하여 뷰를 구성합니다.

NotificationCell

Image Text 를 사용하여 뷰를 구성합니다.

Profile

Profile화면이 완성된 모습입니다.

  • Post, Followers, Following 수를 UserStatView와 ProfileHeaderView에서 지정합니다.
  • ProfileActionButtonView에서는 상대와의 팔로잉상태에 따라 바뀌는 버튼을 구현합니다.
  • ProfileView에서는 Profile화면에 필요한 각 파일을 호출하고 간격을 맞춥니다.

UserStatView

ProfileHeaderView에서 호출하여 사용할 UserStatView입니다.

Post, Follow, Following 숫자를 표시하는 value상수와,
Post, Follow, Following을 표시할 title상수를 선언합니다.

Text로 font의 속성을 지정해 줍니다.

struct UserStatView: View {
    let value: Int
    let title: String
    
    var body: some View {
        VStack {
            Text("\(value)")
                .font(.system(size: 15, weight: .semibold))
            
            Text(title)
                .font(.system(size: 15))
        }
         .frame(width: 80, alignment: .center)
    }
}

ProfileActionButtonView

완성된 ProfileActionButtonView 화면입니다.
팔로잉 상태에 따라 Edit Profile, Follow, Following 버튼이 바뀌는 모습을 구현하였습니다.

목표
조건문, 삼항연산자를 통해 3가지 상태의 버튼을 나타나게 합니다.

코드와 함께 보며 설명드리겠습니다.

import SwiftUI

struct ProfileActionButtonView: View {
    var isCurrentUser = false
    var isFollowed = true
    
    var body: some View {
        if isCurrentUser {
            Button(action: {}, label: {
                Text("Edit Profile")
                    .font(.system(size: 14, weight: .semibold))
                    .frame(width: 330, height: 32)
                    .foregroundColor(.black)
                    .overlay(
                    RoundedRectangle(cornerRadius: 3)
                        .stroke(Color.gray, lineWidth: 1)
                    )
            })
        } else {
            HStack {
                Button(action: {}, label: {
                    Text(isFollowed ? "Following" : "Follow")
                        .font(.system(size: 14, weight: .semibold))
                        .frame(width: 165, height: 32)
                        .foregroundColor(isFollowed ? .black : .white)
                        .background(isFollowed ? Color.white : Color.blue)
                        .overlay(
                        RoundedRectangle(cornerRadius: 3)
                            .stroke(Color.gray, lineWidth: isFollowed ? 1 : 0)
                        )
                })
                Button(action: {}, label: {
                    Text("Message")
                        .font(.system(size: 14, weight: .semibold))
                        .frame(width: 165, height: 32)
                        .foregroundColor(.black)
                        .overlay(
                        RoundedRectangle(cornerRadius: 3)
                            .stroke(Color.gray, lineWidth: 1)
                        )
                })
            }
        }
    }
}

만드는 순서

  1. isCurrentUser, isFollowed 변수를 false로 선언합니다.
 var isCurrentUser = false
 var isFollowed = false
  1. 조건문으로 isCurrentUser값이 트루이면 isCurrentUser를,
    아니면 Follow 또는 Flollwing과 Message을 보여줍니다.
  1. isFollowed가 true이면 Following을, false면 Follow 를 나타냅니다.

삼항연산자

ProfileHeaderView

  • VStack, HStack Image Text Spacer 를 활용하여 View를 구성합니다.
  • UserStatView를 호출, UserStatView에서 선언했던 value, title값을 각각 지정하여 줍니다.
  • ProfileActionButtonView를 호출합니다.

ProfileView

ScrollView 를 사용합니다.
화면에 필요한 파일들을 호출하여 ProfileView를 완성합니다.

 ScrollView {
            VStack(spacing: 32) {
                    ProfileHeaderView()
                    
                    PostGridView()
            }.padding(.top)
        }.clipped()

UploadPost

UploadPost화면에서는 ImagePickerOptional 문법을 사용하여 아래 사진들의 화면을 구현하겠습니다.

UploadPostView

만드는 순서

  1. 버튼을 만들고, 버튼의 레이블은 Image로 plus.square를 사용했습니다.
    이 버튼은 nil과 같을 때에만 나타나게 해줍니다.
    postImage변수는 Image에 옵셔널 지정 해주었습니다.
    밑의 조건문을 보면 postImage가 nil과 같을 때 버튼이 나타납니다.
    즉, 버튼이 생기기전에는 Image도 없기 때문에 postImage 변수에는 값이 없는 상태입니다.
  1. imagePickeerPresented 변수를 false로 선언합니다.
    ImagePicker를 띄우는 지에 대한 변수입니다. true일 경우 ImagePicker를 띄웁니다.
    1번의 버튼 액션은 imagePickerPresented.toggle()입니다.
    플러스 버튼을 누르면 imagePickeerPresented변수가 true로 변경됩니다.

  1. .sheet
    isPresented의 값이 true일 때 content를 보여줍니다.
    ImagePicker의 image에 selectedImage를 넣어 선택된 이미지가 selectedImage에 저장되도록 합니다.
    onDismiss: loadImage → 이미지피커가 사라질 때 loadImage 함수를 실행합니다.

  1. 이미지 피커가 사라질 때 loadImage함수가 실행됩니다.
    selectedImage가 UIImage타입이므로 Image 타입으로 변환하여 postImage로 저장하는 함수입니다.

  1. 4번까지 실행되면 postImage에 값이 생깁니다.
    값이 있으면 아래 사진의 뷰가 나타납니다.

작성한 코드는 아래와 같습니다.

extension

클래스, 구조체, 열거형, 프로토콜 타입 등에 새로운 기능을 추가하여 확장할 수 있는 기능

클래스의 상속과 익스텐션 차이

Class
클래스의 상속은 클래스 타입에서만 가능합니다.
특정 타입을 물려받아 하나의 새로운 타입을 정의하고 추가 기능 구현하는 수직 확장

extensios
구조체, 클래스, 프로토콜 등에 적용 가능합니다.
기존 타입에 기능을 추가하는 수평 확장

기본 형태

extension 확장할 타입 이름 {
     //기능 구현
}

guard let

guard let 은 Optional 값을 Unwrapping 하는 과정입니다.

guard let 범위의 코드가 nil이라면 else구문이 실행됩니다.
값이 있다면 guard let 범위의 코드가 실행됩니다.

if let

if let은 Optional 값을 Unwrapping 하는 과정입니다.

간단하게 if문 안의 조건문의 값이 nil인가 아닌가를 체크하는 문법입니다.
조건문이 nil이 아니라면 해당 블럭이 실행되는 구조입니다.

참고, 인용 자료

TabView
https://seons-dev.tistory.com/entry/SwiftUI-TabView

NavigationView
https://zeddios.tistory.com/986

ScrollView
https://seons-dev.tistory.com/entry/ScrollView

LazyV(H)stack
https://seons-dev.tistory.com/entry/SwiftUI-Lazy-VHStack

Image
https://seons-dev.tistory.com/entry/Image

V(Z, H)Stack
https://hyerios.tistory.com/153

spacing, Spacer
https://flaviocopes.com/swiftui-spacing/

padding
https://huniroom.tistory.com/entry/iOSSwiftUI-padding-정리

Text
https://seons-dev.tistory.com/entry/SwiftUI-Text

접근제어자
https://hongssup.tistory.com/334

LazyV(H)Grid & GridItem
https://seons-dev.tistory.com/58

UIScreen.main.bounds.width
https://borabong.tistory.com/10

Button
https://seons-dev.tistory.com/24

NavigationLink
https://fwani.tistory.com/16

@State & @Binding & $
https://daesiker.tistory.com/49
https://seons-dev.tistory.com/entry/SwiftUI-2-Hello-Binding

overlay
https://seons-dev.tistory.com/entry/Overlay-Background-Alignment

frame
https://developer.apple.com/documentation/swiftui/form/frame(minwidth:idealwidth:maxwidth:minheight:idealheight:maxheight:alignment:)

삼항연산자
https://seons-dev.tistory.com/entry/Swift-기초문법19-삼항-연산자-ternary-operator

옵셔널
https://seons-dev.tistory.com/entry/Swift-기초문법2-Optional-옵셔널

연산자
https://jusung.gitbook.io/the-swift-language-guide/language-guide/02-basic-operators

ImagePicker

https://velog.io/@leeesangheee/ImagePicker-사용하기

Extensions

https://seons-dev.tistory.com/entry/Swift-기초문법43-Extensions

if let, guard

https://seons-dev.tistory.com/entry/SwiftUI-If-let과-Guard

forEach

https://babbab2.tistory.com/95?category=828998

0개의 댓글