ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • swift 로 메모장 만들기 따라하기
    IOS 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의 신뢰성 인증을 받아야하는 번거로움이 있지만, 그만큼 보안이 철저한 것 같다.

    'IOS' 카테고리의 다른 글

    mac에서 httpd 찾기  (0) 2023.12.14
Designed by Tistory.