swift 로 메모장 만들기 따라하기
SWIFT 언어를 공부하기 시작하면서, 유투브 강의를 하나 따라 해보았다.
https://www.youtube.com/watch?v=O7fZ2ZvEKoA
xcode에서 swift를 써본적이 없고 기본적인 문법의 지식이 없었지만 swift를 활용하여 간단한 메모앱을 만들 수 있었고
react를 많이 사용해보지 않은 나로서는 공부하기 좋은 동영상이었다.
이 동영상에서는 Swift UI를 사용했다. Swift UI는 Swift 언어를 이용해서 쉽게 프로덕트를 제작하기 위해서 개발한 프레임워크이다.
해당 프레임워크를 활용해서 제작한 어플의 경우 네이티브 앱이라고 볼 수 있다.
이후에는 회사의 상황에 맞춰서 UI kit 과 cocoapots에서 다운받은 pots 를 활용하여 앱을 개발할 것 같다.
1. 메모 객체 생성
다음과 같이, swift 프로잭트를 생성하니 App과 Test 폴더들이 생성됐다.
이후에 View / Memory / CoreData 폴더를 생성했다.
CoreData에는 프로젝트 생성 시 설정했던 CoreData 로 인해 생성된 두 파일을 넣어뒀다.
아직 자세히는 모르지만, 하나는 DB를 이용할 수 있도록 하는 장치이고 하나는 간편한 DB 인 것 같다.
메모장이므로, 메모와 메모들을 배열 형태로 저장할 수 있는 Store를 생성했다.
import Foundation
import SwiftUI
class Memo: Identifiable, ObservableObject {
let id: UUID
@Published var content: String
let insertDate: Date
init(content:String , insertDate: Date = Date.now){
id = UUID()
self.content = content
self.insertDate = insertDate
}
}
Memo의 경우, id 와 내용, 날짜 3가지의 필드를 가지고 있고, 날짜의 경우 값이 없다면 현재 날짜로 저장되도록 되어있다.
ObservableObject의 경우 옵저버 옵션이 적용되어있어서 해당 객체에 변화가 생기면, 객체와 연결된 다른 요소들에 이벤트가 발생하게 만드는 것 같다.
@Published 로 선언한 변수의 경우, 변수의 값이 연동되고 있는 것 같다.
import Foundation
class MemoStore: ObservableObject {
@Published var list: [Memo]
init(){
list = [
Memo(content: "Fist Content", insertDate: Date.now),
]
}
func insert(memo: String){
list.insert(Memo(content: memo), at: 0)
}
func update(memo: Memo?, content: String){
guard let memo = memo else {
return
}
memo.content = content
}
func delete(memo: Memo){
list.removeAll { $0.id == memo.id }
}
func delete(set: IndexSet){
for index in set {
list.remove(at: index)
}
}
}
MemoStore의 경우 Memo들을 저장할 수 있는 배열이 있고, init 할 때 list를 초기화 한다.
또한 list에 memo를 추가, 수정, 삭제 할 수 있는 function 들이 있다.
모델과 컨트롤러를 잘 설계한 것 같다.
2. 메인 뷰 생성
import SwiftUI
@main
struct SwiftUIMemoApp: App {
@StateObject var store = MemoStore()
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
MainListView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(store)
}
}
}
위의 코드는 앱의 시작점으로, MemoStore 를 초기화하고 메인 뷰를 불러온다.
특이한 점으로는 어노테이션을 StateObject로 걸고 변수를 선언했다. swift에서는 enum처럼 타입에 엄격한 경우들이 많은 것 같은데, 이 부분은 typescript를 사용하는 것 같은 느낌이 든다.
View를 그릴 때는, Scene 위에 View들을 쌓는 느낌이다.
body를 Scene 타입으로 생성 한 후에, 다른 view 파일을 불러서 View타입의 body들을 나열한다.
메인 화면을 그릴 MainListView 에는 .environmentObject(store)를 함으로서, MemoStore를 사용할 수 있게 만들었다.
persistenceController와 .environment의 경우, CoreData와 관련있어 보이는데 자세한 것은 다음에 알아볼 것이다.
import SwiftUI
struct MainListView: View {
@EnvironmentObject var store: MemoStore
@State private var showComposer: Bool = false
var body: some View {
NavigationView {
List {
ForEach(store.list){ memo in
NavigationLink{
NavigationView{
DetailView(memo: memo)
}
} label: {
MemoCell(memo: memo)
}
}
.onDelete(perform: store.delete)
}
.listStyle(.plain)
.navigationTitle("내 메모")
.toolbar {
Button {
showComposer = true
} label: {
Image(systemName: "plus")
}
}
.sheet(isPresented: $showComposer){
ComposeView()
}
}
}
}
#Preview {
MainListView()
.environmentObject(MemoStore())
}
메인 뷰 파일은 다음과 같다.
먼저, MemoStore를 사용할 수 있게 필드에 저장하고, Body 안에 요소들을 추가했다.
특이한 점 1번으로는
대부분 swift에서 제공하는 명령어로 제작했다는 점이다.
NavigationView 는 앱 화면에 Toolbar 또는 Navigation 요소에 대한 설정을 할 수 있게 해준다.
부트스트랩을 사용하고 있는 것 처럼, NavigationView안의 공간을 세밀하게 분류해둔 것 같다.
특이한 점 2번으로는 React처럼 컴포넌트에 . 을 붙이는 것으로 뷰를 그린다.
swift에 저장되어 있는 변수를 불러올 때 .Name 형태로 불러오던데, 헷갈리지 않게 조심 해야할 것 같다.
필드에서 선언한 변수를 불러올 때는 $ 를 앞에 붙인다
특이한 점 3번은 버튼 태그에 이벤트를 걸 때, $State 변수를 미리 선언해두고, isPresented 에 해당 변수를 걸어두고 사용한다는 점이다.
아직 이 방식에 대해 완벽하게 이해한 것은 아니지만, 이벤트를 만들 때 마다 변수를 선언하면 복잡한 기능의 경우 변수가 너무 많을 것 같기도 하고 관리를 깔끔하게 하는 것 같기도 하고 반반이다.
마지막으로 #Preview 를 이용하여 시뮬레이터를 켜면, 현재 뷰가 어떻게 작업되고 있는지 확인할 수 있다.
3. 그 외 작은 뷰 들
import SwiftUI
struct MemoCell: View {
@ObservedObject var memo: Memo
var body: some View {
VStack(alignment: .leading){
Text(memo.content)
.font(.body)
.lineLimit(1)
Text(memo.insertDate, style: .date)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
#Preview {
MemoCell(memo: Memo(content: "Test"))
}
MemoCell은 메인 뷰의 List에 들어갈 요소들이다. HTML로 따지면 tr 태그 같다.
요소를 선언하고 .명령어를 추가하는 것으로 코드를 구성하니 매우 깔끔하다.
다만, 반대로 여러가지 코드들을 숙지하고 있어야 편하게 사용할 수 있을 것 같다
import SwiftUI
struct ComposeView: View {
@EnvironmentObject var store: MemoStore
var memo: Memo? = nil
@Environment(\.dismiss) var dismiss
@State private var content: String = ""
var body: some View {
NavigationView {
VStack {
TextEditor(text: $content)
.padding()
.onAppear {
if let memo = memo {
content = memo.content
}
}
}
.navigationTitle(memo != nil ? "메모 편집" : "새 메모")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading)
{
Button {
dismiss()
} label: {
Text("취소")
}
}
ToolbarItemGroup(placement: .navigationBarTrailing)
{
Button {
if let memo = memo {
store.update(memo: memo, content: content)
} else {
store.insert(memo: content)
}
dismiss()
} label: {
Text("저장")
}
}
}
}
}
}
#Preview {
ComposeView()
.environmentObject(MemoStore())
}
ComposeView는 메모 작성 공간이다. 주로 .sheet를 사용하여 모달 형태로 불러오도록 사용되었다.
이 때 사용된 dismiss() 는 주로 모달창이나 시트를 닫을 때 사용되고, NavigationLink로 이동한 경우에도 이전 화면으로 돌아갈 수 있는 기능이다.
이 뷰는 다른 것 보다, memo의 상태에 따라서 텍스트와 기능이 변하는 부분을 깔끔하게 설계한 것 같다.
import SwiftUI
struct DetailView: View {
@ObservedObject var memo: Memo
@EnvironmentObject var store: MemoStore
@State private var showComposer = false
@State private var showDeleteAlert = false
@Environment(\.dismiss) var dismiss
var body: some View {
VStack {
ScrollView {
VStack {
HStack {
Text(memo.content)
.padding()
Spacer(minLength: 0)
}
Text(memo.insertDate, style: .date)
.padding()
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
.navigationTitle("메모 보기")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button {
showDeleteAlert = true
} label: {
Image(systemName: "trash")
}
.foregroundColor(.red)
.alert("삭제 확인", isPresented: $showDeleteAlert){
Button(role: .destructive){
store.delete(memo: memo)
dismiss()
} label: {
Text("삭제")
}
} message: {
Text("메모를 삭제할까요?")
}
Button {
showComposer = true
} label: {
Image(systemName: "square.and.pencil")
}
}
}
.sheet(isPresented: $showComposer){ ComposeView(memo: memo)
}
}
}
#Preview {
NavigationView {
DetailView(memo: Memo(content: "Hello"))
.environmentObject(MemoStore())
}
}
상세보기 페이지 이다. 이 페이지를 보면 확실히 여러가지 명령어들을 다 알고 있어야 개발하기에 용이할 것 같다는 생각이 들었다.
4. 마무리
mac과 xcode를 사용하면 좋은 점은 기본적으로 탑제하고 있는 단축키와 기능이 많다는 것이다.
특히 특정 Element에 우클릭 또는 shift + command + A 를 눌러서 Quick Action을 켜면,
특정 요소가 사용되고 있는 모든 문서의 이름을 변경하는 Rename이나,
요소에 Container를 생성하는 Embed,
여러가지 Stack을 생성하는 기능 등 개발에 용이한 기능들이 많다.
5. 아이폰에 내려받기
Xcode의 상단에 있는 메뉴 탭에서 Mac과 열결된 I phone을 선택하고, build 버튼을 클릭하면 핸드폰에 어플을 다운받을 수 있다.
물론, 그 전에 Xcode 에서 Apple계정과 iPhone의 신뢰성 인증을 받아야하는 번거로움이 있지만, 그만큼 보안이 철저한 것 같다.