시작하기에 앞서
본 포스팅은 Udemy 클론코딩 강의를 보고 작성하였습니다.
Instagram SwiftUI Clone | MVVM | Cloud Firestore
강의는 1 ~ 15강 까지를 정리하였고, MainTabView 와
Feed, Search, UploadPost, Notification, Profile
인스타그램 5개 화면을 구성하는 내용을 담고 있습니다.
포스팅 내용은 이렇습니다.
화면을 구성하는 과정에서 미완성 뷰를 호출하는 작업이 많습니다.
하여 필요한 파일들을 먼저 생성하고 진행토록 하겠습니다.
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화면이 완성된 모습입니다.
TabView기능을 사용하여 인스타그램 하단의 탭바를 구현하겠습니다.
TabView 구현 순서
TabView {
FeedView()
}
- 이미지가 없는 상태의 탭바가 생성됩니다.
TabView {
FeedView()
.tabItem {
Image(systemName: "house")
}
}
Image 아래 문자열 표기
Text, label 둘 중 편한 것을 사용하면 됩니다.
TabView {
FeedView()
.tabItem {
Text("Feed")
Image(systemName: "house")
}
}
TabView {
FeedView()
.tabItem {
Label("Feed", systemImage: "house")
}
}
TabView가 무엇인지, 어떻게 사용하는지 알아보았습니다.
본격적으로 인스타그램의 탭뷰를 구현해보겠습니다.
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)
}
Feed화면이 완성된 모습입니다.
Feed화면은 FeedCell, FeedView파일에서 코드를 작성하여 구현합니다.
FeedCell이 완성된 모습입니다.
FeedCell은 밑의 각 기능들의 설명을 보고 따라만드시면 됩니다.
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이미지 우측으로 생성 되었습니다.
정렬은
.leading
.center
.trailing
이 있습니다.
위 사진에서 계정사진은 왼쪽에 위치합니다.
이는 .leading
정렬을 해주었기 때문입니다.
VStack (alignment: .leading)
정렬을 해주지 않는다면 밑의 사진과 같은 모습이 됩니다.
이미지는 쓰임새에 맞게 다양한 방법으로 조정, 조절 할 수 있습니다.
이미지를 아래와 같이 원형으로 만들 때 필요한 속성들 입니다.
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("donkey")
.font(.system(size: 14, weight: .semibold))
인스펙터에서 Text의 다양한 속성을 변경할 수 있습니다.
현재 버튼의 액션은 없지만 만들어 두고 눌러봅니다.
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 에서는 ScrollView, LazyVStack, ForEach를 사용합니다.
- 만들었던 FeedCell을 밑의 코드와 같이 ForEach함수 내에서 호출하면 Feed화면이 완성됩니다.
스크롤 가능한 콘텐츠 영역 내에 콘텐츠를 표시합니다.
가로, 세로 또는 둘 다 스크롤할 수 있지만 확대/축소 기능은 제공하지 않습니다.
LazyVStack은 VStack과 스크롤바 범위 차이가 있습니다.
VStack일때는 텍스트가 끝나는지 점에 스크롤바가 생성됩니다.
LazyVStack은 화면의 넓이에 맞게 스크롤바가 생성됩니다.
LazyVStack은 스크롤할 때마다 데이터를 로드해 주지만
VStack은 데이터를 모두 다 추가합니다.
최적화 차이가 있다는 말과 같습니다.
forEach는 반복하고 싶은 구문(FeedCell())을
forEach라는 함수의 매개변수(Parameter) "클로저"로 작성해서 넘겨주는 것 입니다.
ForEach(0 ..< 10) { _ in
FeedCell()
}
** 함수의 괄호() 안에 들어가는 것을 매개변수(Parameter) 라고합니다.
func printForIn() {
let nums = [1, 2, 3]
for num in nums {
print(num)
return
}
}
for - in의 경우 반복문을 돌다가 return을 만나면 함수 자체가 종료됩니다.
따라서 위 함수를 실행할 경우, 프린트 값은 1
func printForEach() {
let nums = [1, 2, 3]
nums.forEach {
print($0)
return
}
}
forEach는 내가 전달한 클로저를 요소 갯수 만큼 실행 하기 때문에
반복 횟수에 영향을 주지 않습니다. 프린트 값은 1, 2, 3
PostGridView는 Search, Profile화면에 쓰입니다.
LazyVGrid
ForEach
NavigationLink
의 개념과 사용방법을
아래 코드와 함께 보며 설명하겠습니다.
- GirdItem이 있는 items상수를 선언합니다.
- 디바이스의 너비값의 1 / 3인 width상수를 선언합니다.
- LazyVGrid를 만들고 columns값엔 items를, spacing으로 수직 간격을 조절합니다.
- content 안에는 ForEach로 목적지가 FeedView, 이미지가 있는 NavigarionLink를 순차적으로 나타내줍니다.
private 사용시 같은타입 안에서만 사용가능하며 그외에 어떠한 상황에서도 접근 불가능 합니다.
UIScreen.main.bounds.size.width 를 이용하면 디바이스의 너비값을 알 수 있습니다.
SwiftUI 에서 Grid 뷰를 그릴 때 LazyVGrid 또는 LazyHGrid 를 사용합니다.
LazyVGrid : 위아래로 스크롤
LazyHGrid : 좌우로 스크롤
LazyVGrid를 만들땐 GridItem이 반드시 필요합니다.
목적지 뷰를 설정하여 누르면 넘어갈 수 있게합니다.
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()
}
- HStack 안에 이미지를 넣습니다.
- HStack 안에 Vstack을 넣고 2개의 Text를 만듭니다.
- HStack 안에 Spacer를 넣어 좌우 간격을 벌립니다.
이미지와 이미지 사이에 Spacer() 를 써주면 간격을 최대한 띄웁니다.
ScrollView
LazyVStack
ForEach
NavigationLink
를 사용하고,
UserCell을 호출 하여 완성합니다.
코드와 함께 보며, 이 전까지 다루었던 내용들을 참고하여 만들면 됩니다.
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를 보여줍니다.
- SearchView
ScrollView내 SearchBar를 호출합니다.- SearchView
ZStack 내 UserListView, PostGridView를 호출합니다.
1, 2번의 이해를 돕기위한 코드
ScrollView {
SearchBar()
.padding()
ZStack {
UserListView() //
PostGridView() //
}
}
- 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)
- 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)
일반적으로 Struct는 값 타입이라 Struct 안에 있는 값을 변경할 수 없습니다.
그래서 만든 것이 @State 키워드, 이는 struct 내에서 값을 변경가능하도록 도와줍니다.
자신의 뷰가 아닌 부모의 뷰에서 값을 가져오고 싶을 때 사용하는 키워드입니다.
뷰끼리 양방향으로 연결 해서 서로 값을 공유할 수 있도록 도와주는 키워드 입니다.
정리
@State는 주가되는 View에 한번 선언합니다.
이 프로퍼티를 다른 뷰에서 사용하기위해 @Binding을 사용합니다.
그리고 사용시에는 $를 앞에 붙여 Binding변수 임을 나타냅니다.
View UI에서 특정 동작으로 해당 프로퍼티 값이 변경되는 자리에는 앞에 $를 붙여줍니다.
.overlay 를 사용하여 돋보기 이미지도 넣어줍니다.
5 ~ 11번의 내용입니다.
- @State var isSearchMode = false 선언합니다.
- ZStack 으로 조건문을 작성한 뒤 false, true 값을 바꾸며 PostGridView, UserListView가 나타나는지 확인합니다.
- SearchBar에서 @Binding var isEiting: Bool 선언합니다.
- SearchView에서 호출한 SearchBar 내에 isEditing: $isSearchMode 지정합니다.
- TextField에 onTapGesture를 사용합니다.
TextField를 탭하면 isEditing = true 가 되어 UserListView가 나타납니
다.- 조건문 isEditing이 true이면 버튼이 나타납니다.
이 버튼을 누르면 isEditing = false가되어, PostGridView가 나타납니다.
label은 Cancel로 지정합니다.
text = "", 버튼을 누르면 PostGridView가 나타나며 TextField를 빈 값으로 만들어 줍니다.
알림화면 입니다.
ScrollView
LazyVStack
ForEach
을 사용하고,
NotificationCell 을 호출하여 뷰를 구성합니다.
Image
Text
를 사용하여 뷰를 구성합니다.
Profile화면이 완성된 모습입니다.
- Post, Followers, Following 수를 UserStatView와 ProfileHeaderView에서 지정합니다.
- ProfileActionButtonView에서는 상대와의 팔로잉상태에 따라 바뀌는 버튼을 구현합니다.
- ProfileView에서는 Profile화면에 필요한 각 파일을 호출하고 간격을 맞춥니다.
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 화면입니다.
팔로잉 상태에 따라 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)
)
})
}
}
}
}
만드는 순서
var isCurrentUser = false
var isFollowed = false
VStack, HStack
Image
Text
Spacer
를 활용하여 View를 구성합니다.- UserStatView를 호출, UserStatView에서 선언했던 value, title값을 각각 지정하여 줍니다.
- ProfileActionButtonView를 호출합니다.
ScrollView
를 사용합니다.
화면에 필요한 파일들을 호출하여 ProfileView를 완성합니다.
ScrollView {
VStack(spacing: 32) {
ProfileHeaderView()
PostGridView()
}.padding(.top)
}.clipped()
UploadPost화면에서는
ImagePicker
와Optional
문법을 사용하여 아래 사진들의 화면을 구현하겠습니다.
만드는 순서
- 버튼을 만들고, 버튼의 레이블은 Image로 plus.square를 사용했습니다.
이 버튼은 nil과 같을 때에만 나타나게 해줍니다.
postImage변수는 Image에 옵셔널 지정 해주었습니다.
밑의 조건문을 보면 postImage가 nil과 같을 때 버튼이 나타납니다.
즉, 버튼이 생기기전에는 Image도 없기 때문에 postImage 변수에는 값이 없는 상태입니다.
- imagePickeerPresented 변수를 false로 선언합니다.
ImagePicker를 띄우는 지에 대한 변수입니다. true일 경우 ImagePicker를 띄웁니다.
1번의 버튼 액션은 imagePickerPresented.toggle()입니다.
플러스 버튼을 누르면 imagePickeerPresented변수가 true로 변경됩니다.
- .sheet
isPresented의 값이 true일 때 content를 보여줍니다.
ImagePicker의 image에 selectedImage를 넣어 선택된 이미지가 selectedImage에 저장되도록 합니다.
onDismiss: loadImage → 이미지피커가 사라질 때 loadImage 함수를 실행합니다.
- 이미지 피커가 사라질 때 loadImage함수가 실행됩니다.
selectedImage가 UIImage타입이므로 Image 타입으로 변환하여 postImage로 저장하는 함수입니다.
- 4번까지 실행되면 postImage에 값이 생깁니다.
값이 있으면 아래 사진의 뷰가 나타납니다.
작성한 코드는 아래와 같습니다.
클래스, 구조체, 열거형, 프로토콜 타입 등에 새로운 기능을 추가하여 확장할 수 있는 기능
Class
클래스의 상속은 클래스 타입에서만 가능합니다.
특정 타입을 물려받아 하나의 새로운 타입을 정의하고 추가 기능 구현하는 수직 확장
extensios
구조체, 클래스, 프로토콜 등에 적용 가능합니다.
기존 타입에 기능을 추가하는 수평 확장
extension 확장할 타입 이름 {
//기능 구현
}
guard let 은 Optional 값을 Unwrapping 하는 과정입니다.
guard let 범위의 코드가 nil이라면 else구문이 실행됩니다.
값이 있다면 guard 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
삼항연산자
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