IOS

swift 로 메모장 만들기 따라하기

hojomu 2023. 12. 14. 17:06

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의 신뢰성 인증을 받아야하는 번거로움이 있지만, 그만큼 보안이 철저한 것 같다.