회사에서 서비스하는 앱이란건 보통 서버에서 데이터를 저장, 관리하기에 앱 단독으로 데이터 저장은 거의없었다.

개인 로그인 기록이나 편의성을 위한점때문에 userdefaults같은걸 써서 저장하거나 따로 파일을 만들어 저장하는게 편하기에 굳이 새로운 라이브러리를 넣어서 무겁게 돌릴필요가 없었다

그래도 방법은 있으니 혼자서 공부삼아 만들어보는 예제에서는 sqlite를 쓰거나 firebase를 로컬로쓰거나(듣기만했지 만들어보지도 찾아보지도 않았다.) 아니면 애플에서 제공하는 프레임워크인 Coredata를쓰거나! 한다.

그렇게 이글은 애플에서 제공하는 데이터 저장용 프레임워크인 coredata를 uikit이아닌 swiftui로 만들어 붙이는걸 공부하는 글이다.

 

 

4월달 내로 만들어서 내이름으로 처음 등록하려던 앱계획이 있었다. 하지만 정신상태가 흔들리는 일들을 계속 맛보고있는 와중이라 여러번 의욕이 꺽이면서 아직도 한참 멀었다 그앱은 자금사정상 서버를 이용한 기능은 빼고 로컬에서만 돌아가도록할건데 여기서 데이터가 영속적으로 유지해야하는 기능이 존재한다. 

데이터가 영속적으로 존재하려면 위에서 쓴 서버를 이용하거나 텍스트파일로 쓰거나, 로컬db용 라이브러리를 쓰거나해야하는데 자금문제로 서버는 무리고 텍스트파일로 쓰기에는 뭔가 공부도 안될거같아 로컬db로 목표, 그리고 이왕이면 안해본걸 잡아보고싶어서 coredata로 하기로 마음먹었다.

기억이 가물가물하지만 보통 로컬디비가 보통 시작이 이랬을거다. 

1. 디비용 클래스 초기화

2. 초기화시 디비 파일이 없다면 생성 및 스키마 뭐 그런 초기값들 설정

3. 디비 파일이 있다면 로드준비들

4. 해당 기능들을 래핑한 컨트롤러의 생성

5. 이제 컨트롤러를 이용해서 필요할때 백그라운드 스레드로 돌리면서 쿼리를 날리며 ui업데이트

이런식이였을거다.

 

프레임워크를 그냥 제공해주기에 별다른설치나 그런 고민없이 간단히 쓸수있을거라 생각하고 자료좀 찾아봤는데 뭔가 말이많다...

처음 아이폰개발 배웠을때도 이랬었나 고민하게되는 뭔가뭔가들이라 쉽게 손이 안가 그냥 튜토리얼을 따라하며 감을 잡아야겠다 싶어 따라했다

그렇게 대충 배운 내용으로는 

아래와같다

1. 프로젝트 내에 엔티티 정의용 모델파일을 생성한다 - xcdatamodeld 라는 확장자를 가진 파일

2. 정의용 모델파일에서 스키마를 설정 - 서버용의 dbms에서 gui로 편하게 만드는 엔티티라는 느낌이였다

3. 컨트롤러 생성, 컨트롤러지만 컨트롤 기능정의가 아니라 단순 모델파일 로드 - 컨테이너생성

4. enviroment modifier로 managedObjectContext 키값에 만들어둔 컨트롤러에서 컨테이너의 뷰 컨텍스트 지정 

ContentView()
	.environment(\.managedObjectContext, presistenceController.contianer.viewContext)

5. 디비 접근이 필요한곳에서 viewContext 받아주기 + 저장된 값 불러오기

@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(entity: Product.entity(), sortDescriptors: [ 값 정렬 ]
private var products: FetchedResults<Product>

뭐 그렇게 읽고

쓸때는 저기 Product라고 만들어준 엔티티를 생성해줘서 위에서 받아온 viewContext를 이용해 save() 메소드를 호출하면된다.

  private func addProduct() {
    withAnimation {
      let product = Product(context: viewContext)
      product.name = name
      product.quantity = quantity
      
      saveContext()
    }
  } // addProduct
  
  private func saveContext() {
    do {
      try viewContext.save()
    } catch {
      let error = error as NSError
      fatalError("An error occurred: \(error)")
    }
  }// saveContext

참고로 여기서 Product 타입은 엔티티 만드는곳에서 자동으로 생성해서 쓰고있다

이걸 따로 수동으로 만들어서 해줄수도 있음.

수동으로하려면 엔티티 생성해주는곳에서 메뉴얼을 선택해줘야한다. 아래 사진의 Codegen부분이다 Manual과 CategoryExtension이라는게 있던데 카테고리 뭐시기는 아직 안해봤다.

 

위에서 데이터 Fetch해줄때 그냥 전부 가져왔는데 조건문을 넣어서 불러오는것도 당연히 있다.

예제에서는 task 수정자를 이용해 하도록 안내해줬다

.task {
        let fetchRequest: NSFetchRequest<Product> = Product.fetchRequest()
        fetchRequest.entity = Product.entity()
        fetchRequest.predicate = NSPredicate(format: "name CONTAINS %@", name)
        matches = try? viewContext.fetch(fetchRequest)
      }

contain연산자를 이용한 name 검색인데 별건 없다

자동으로 만들어준 Product라 그런가 별에 별게 다 미리 지정되어있다 fetchRequest라던가...

사실 이건 내부 들어가보면 별거 없긴한데 자동이라 편하다.

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Product> {
        return NSFetchRequest<Product>(entityName: "Product")
    }

 

그리고 마지막으로 crud에서 update는 안다룰거고... delete는 뭔가 해준 느낌이 안난다.

      offsets.map { products[$0] }.forEach(viewContext.delete)
      saveContext()

값을 list로 뿌려줬으니 offset값을 바로 가져올수있고 받아온 offset값에 대응하는 products값에서 delete메소드를 불러준후 save 메소드를 불러주는거로 끝이난다

 

기존 로컬디비 다루는법에서 다른점은 

컨트롤러에서의 해주는게 사용하려는 클래스? 스트럭트? (swiftui는 죄다 구조체로 선언해서 용어적으로 뭘 불러야할지 모르겠다 )

사용하려는곳에서 프로퍼티래퍼를 달아줘서 기능을 사용한다는 점일까?

 

일단 이론적인 이야기로는

앱 -

영구컨테이너 -

관리 객체 콘텍스트 ~ 관리 객체 ~ 관리 객체 모델 ~ 엔티티 디스크립션 -

영구 저장소 코디네이터

영구 저장소 객체

데이터베이스

이런 레이어들이 쌓여서 서로 부르고 알고 모르니 하는 설명이 뭔가 많다.

예제에서 다루는 부분을 보면 우선 코디네이터 아래단은 전부 프레임워크에 역할을 맡기는거 같고

영구 컨테이너는 컨트롤러에서 생성, 그 생성할 관리 객체단이 xcdatamodeld파일이라는 느낌? 

반응형

0. 시작

한참동안 또 안건드리다가 다시 잡게되었다.

이전에 잠금화면에서 재생화면으로 뜨는거와 mvvm으로 나눠둔것까지 했었다. 기능적으론 딱히 더 필요한게 재생바에서 재생시간옴기는거려나 그럼 이거 추가하는김에 SwiftUI를 공부도 하려니 uikit부분을 swiftui로 변경할거다.

SwiftUI 뷰만 만들어봐야 별로 다를것없어보이니 기존과 꽤나 다른점인 @Published 이게 눈에 보여서 @EnviromentObject이걸 뷰모델에 넣어서 뷰와 연결해볼거다

@EnviromentObject는 컨테이너시스템에서 시스템 시작할때 환경변수파일 넣어주듯이 넣는 개념이라 이해하고있다

이 객체로 선언된건 바로 뽑아와서 쓰고 Published 태그가 붙은 변수들은 변경될때마다 이벤트를 발생시켜 구독중인 애들이 새로 그려진다는듯하다

이것만들으면 그냥 notification을 변수의 set 항목에 지정해서 쓴거아닌가 싶은데 뭐 아무튼 설명이 그러니 그냥 쓰려고한다

 

1. 기존 프로젝트에서 어떻게 바꿀까?

기존 파일 구성이

ControllViewController.swift

MusicInfo.swift

MusicPlayer.swift

NewMainVC.swift

PlayList.swift

PlayListCell.swift

였는데

NewMainVC가 겉으로 

여기에 테이블을 넣고 테이블셀은 PlayListCell로 지정해서 모양을 잡아줬고

테이블 아래로는 ControllViewController를 집어넣어서 위에는 파일리스트 아래는 재생버튼으로 뷰를 마무리 하고

실 플레이 기능은 MusicPlayer와 음악데이터값은 PlayList에 나눠서 구성했었다.

기능별 분리를 해놔서 swiftUI로 변경은 어렵지 않을거라 생각했건만 

실 기능과 데이터가 있는 MusicPlayer와 PlayList 클래스를 ObservableObject를 달아줘서 밖으로 쓸 변수를 @Pbulished를 해줬다

그리고 이건 망했다 왜 망했는지는 뷰를 그리고 나서 이야기하자

 

뷰는 일단 따로 ai로 그려진걸 그려볼까했는데 막상 하려니 내가 원하지 않는 기능들이 덕지덕지 붙은 그림을 줘서 그냥 포기.

기존과 비슷하게 그러나 swiftUI기본 디자인이 워낙 괜찮아서 같은 위치에 같은 기능이나 기존보다 살짝 더 디자인적으로 나아졌다

우선 가장 겉은 부분은

MainScreen 이라는 이름으로 뷰를 그려줬다 기존 방식그대로 위에는 리스트, 아래는 컨트롤바

VStack(spacing: 0) {
    PlayListView()
    ControllBarView()
}

기본 스페이싱을 없애기위애 따로 spacing 0을 넣어줬다.

 

PlayListView는 뭐 별다를거 없는데

  var body: some View {
    List(player.musicData) { item in
      Text(item.title)
    }
    .listStyle(.automatic)
    .onAppear(perform: {
      player.loadList()
    })
  }

셀지정이나 그런것도 없이 바로 Text를 이용해 뿌려주면 끝! 너무 간단해서 이걸 굳이 나눴어야 했나 싶을 정도로 끝이 났고

생각해보니 원본도 테이블뷰를 메인에 넣었던거같아서 실수했다. 

 

아래는 컨트롤바로 기존과 다른점이라곤 버튼에 배경을 넣어줬다

원래는 자리차지가 어느정도 되나 집어넣은 테스트 색상이였는데 보다보니 괜찮아보여 그냥 냅뒀다

  var body: some View {
    VStack(
      alignment: .leading,
      spacing: 4,
      content: {
        Button(action: {
          player.loadList()
        }, label: {
          Text("내부파일 재로드")
        })
        .padding(.horizontal)
        .foregroundColor(.white)
        .background(Color.blue)
        .cornerRadius(10)
        .font(Font.title3)
        
        HStack(content: {
          Text("\(minuteString(to: player.currentTime))")
          Slider(value: $player.currentTime, in: 0...player.duration) { isEditing in
            if !isEditing {
              player.seek(to: player.currentTime)
            }
          }
          Text("\(minuteString(to: player.duration))")
        })
        .padding(.horizontal, 8)
        
        HStack(content: {
          Spacer()
          
          ControllButton(text: "❮") {
            _ = player.previousMusic()
          }
          ControllButton(text: player.isPlaying ? "❚❚" : "▶︎") {
            _ = player.play()
          }
//          .disabled(true)
          ControllButton(text: "❯") {
            _ = player.nextMusic()
          }
          
          Spacer()
        })// HStack
        
        Color.clear
          .frame(height: 8)
      })
  }
  
  struct ControllButton: View {
  let text: String
  let action: () -> Void
  
  var body: some View {
    Button(action: action) {
      Text(text)
        .font(.title)
        .frame(width: 50)
        .foregroundColor(.white)
        .background(Color.blue)
        .cornerRadius(10)
    }
  }
}

뷰 파일을 이렇게 만들려고했으면 기존엔 앵커로 엄청 줄줄이 써댔거나 snapkit으로 그나마 짧게 잡아줬을텐데

이런거 보면 정말 간단해졌다.

 

 

2. 뷰를 만들어주고 연결해주려니 에러가 났다.

1의 뷰랑 연결시켜야하는 기존의  MusicPlayer와 PlayList 클래스에대한 문제는...

MusicPlayer에서 PlayList를 참조한다는점에서 문제가 발생했다

 

--

ControllBarView.swift:27:36: error: cannot find type 'PlayList' in scope

@EnvironmentObject var playList: PlayList

--

둘다 ObservableObject로 만들어주고 처음화면의 뷰에다가 

.environmentObject(musicPlayer)

.environmentObject(playList)

이렇게 두개를 주입해줬는데 musicPlayer에서 playlist를 찾지못한다는 에러가 발생 했다 처음하는 swiftUI라 뭔가 잘못했구나 싶어 gpt에게 열심히 물어봤지만, 무료분량이 끝날때까지 이놈은 이상한 소리만하고 결국 고쳐지지 못했었다

그리고 누군가 올린 블로그글에서 문제를 찾았는데....

뷰모델에선 뷰모델을 부르지 못한다고한다

단순히 notifiation이라고 생각하고 있던 내가 아 잘못이해했구나 한걸 깨달게되는 사건이였다

싱글톤에서 벗어나 바인드도 하고하는 좀 rx스타일같은걸 배우고싶었는데 이제와서 다시 싱글톤으로 하기엔 미묘하다

 

결국 그렇게 난 이 상태를 포기하고 MusicPlayer와 PlayList 클래스를 합쳤다

주입해주던 클래스도 하나로 변경되서

.environmentObject(player)

로 깔끔해졌다.

 

뭐 결국 그냥저냥.... 그렇게 되었다

아 재생바는 slider로 연결해서 값 변경이 완료되면 

  func seek(to time: TimeInterval) {
    avPlayer.currentTime = time
    currentTime = time
  }

이 함수로 변경해주도록 해서 마무리.

뭐 결국 그렇게 테스트해보고 잘됐다.

역시 기본설정들이 다크모드용으로 따로 스타일 지정안해줘도 보기좋게 나온다

리스트도 딱히 만져준게 없는데도 나름 예쁘게 잘나왔고 슬라이더도 잘동작하니 만족

여기서 더 만질일이 있으련지는 모르겠다

나중에 뭔가 추가한다면 리스트에서 선택해서 재생하도록하는거?

그런데 나는 그냥 긴 파일을 다운받아서 재생해놓기에 쓸일이 있으려나

아니면 웹주소로 재생되게하는거?

이건 초기 컨셉이였던거같은데 생각나면 해야지

 

변경된 전체 코드는 역시 내 깃에 ~ 

https://github.com/wiwi-git/proj_ypl/

 

GitHub - wiwi-git/proj_ypl: ios에서 간단하게 쓸 백그라운드 플레이어가 필요하다

ios에서 간단하게 쓸 백그라운드 플레이어가 필요하다. Contribute to wiwi-git/proj_ypl development by creating an account on GitHub.

github.com

 

3. ps

다 쓰고 나니 생각났는데

기존 uikit으로 만든 프로젝트이기에 이 프로젝트에는 Appdelegate와 SceneDelegate파일이 있는 프로젝트다

거기다 추가로 스토리보드 기반이 아니기에 SceneDelegate에서 루트 vc를 설정해준 프로젝트다

 

이걸 시작지점부분을 swfitUI파일과 연결해줘야하는데

swiftUI는 struct이고 view이기에 viewcontroller가 없다 그래서 추가로 따로 만들어줘야한다 이렇게

let hostingController = UIHostingController(rootView: MainScreen())
window?.rootViewController = hostingController

그렇게 만들어주고 기존에 하던데로 루트를 만들어준거로 넣어주면 끝!

 

참고로 mainScreen에는 App 프로토콜뷰가 없다 App은 SwiftUI기반 프로젝트를 생성하면 나오는애

 

반응형

스위프트3을 접하고 몇 년이 지나고 ... 정들었던 UIkit의 자리를 swiftUI가 점점 뺏고 있는와중

나도 swiftUI를 공부해야겠다 싶어 시작한다.

uikit의 내용을 swiftUI와 대조하며 파악해보려한다.

이글은 내 개인의 학습하면서 쓰는 글이라 틀린 정보가 있을 수 있다.

UIWindow -> windowGroup

WindowGroup {
    ContentView()
}


UILabel ->

Text("hello world")


UIButton ->

init(action: @escaping () - Void, @ViewBuilder label: () -> Label)

var body: some View {
    Button {
        print("Tap")
    } label: {
       Text("Tap me!")
    }
}


UITextField ->

init(LocalizedStringKey, text: Binding<String>)

@State private var textValue: String = ""

var body: some View {
    TextField("PlaceHolder...", text: $textValue)
}


UIImageView ->

init(_ name: String, bundle: Bundle? = nil)

var body: some View {
    Image(systemName: "star") // systemImage
}


UITableView ->

init(@ViewBuilder content: () -> Content)

var body: some View {
    List {
        ForEach(0 ..< 10, id: \.self) { i in
            Text("item \(i)")
        }
    }
}


UICollectionView ->

ScrollView + 
init(columns: [GridItem], alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, pinnedViews: PinnedScrollableViews = .init(), @ViewBuilder content: () -> Content)

let layout: [GridItem] = [
    GridItem(.flexible(minimum: 40, maximum: 100)),
    GridItem(.flexible(minimum: 40, maximum: 100)),
]

var body: some View {
    VStack(alignment: .center) {
        ScrollView(.vertical) {
            LazyVGrid(columns: layout) {
                ForEach(0 ..< 100, id: \.self) { index in
                    Text("item \(index)")
                        .frame(height: 30)
                }
            }
        }
        .frame(maxHeight: .infinity)
        .background(.yellow)

        ScrollView(.horizontal) {
            LazyHGrid(rows: layout) {
                ForEach(0 ..< 100, id: \.self) { index in
                    Text("item \(index)")
                        .frame(height: 30)
                }
            }
        }
        .frame(maxHeight: .infinity)
    }
}


UIAlertController style.alert->

View().alert(isPresented: Binding<Bool>, content: () -> Alert) -> some View
View().alert<A>(_ titleKey: LocalizedStringKey, isPresented: Binding<Bool>, @ViewBuilder actions: () -> A) -> some View where A : View

@State private var showingAlert = false

var body: some View {
    Button(action: {
        self.showingAlert = true
    }) {
        Text("Show Alert")
    }
    .alert("title", isPresented: $showingAlert) {
        Button("OK", role: .cancel) {
            print("tap ok")
        }
        Button("NO", role: .destructive) {
            print("tap no")
        }
    }
    /*
    .alert(isPresented: $showingAlert) {
        Alert(title: Text("Title"), message: Text("This is a alert message"), dismissButton: .default(Text("Dismiss")))
    }
    */
}


UIAlertController style.actionSheet

View().actionSheet(isPresented: Binding<Bool>, content: () -> ActionSheet) -> some View

@State var showActionSheet = false
var body: some View {
    Button("sheet") {
        showActionSheet = true
    }
    .buttonStyle(.bordered)

    .actionSheet(isPresented: $showActionSheet) {
        ActionSheet(title: Text("Title"), buttons: [
            ActionSheet.Button.default(Text("text"), action: {
                print("tap text")
            }),
            .default(Text("default")),
            .cancel()
        ])
    }
}


UISwitch ->

init(isOn: Binding<Bool>, @ViewBuilder label: () -> Label)

@State private var toggleValue: Bool = false

var body: some View {
    Toggle(isOn: $toggleValue) {
        Text("토글 텍스트")
    }
}


UIPickerView + UISegmenetedControl( style segmented ) ->

init(_ titleKey: LocalizedStringKey, selection: Binding<SelectionValue>, @ViewBuilder content: () -> Content)
init(selection: Binding<SelectionValue>, label: Label, @ViewBuilder content: () -> Content)

@State var select = "Apple"

var fruits = ["Apple", "Apricot", "Banana", "Pear"]

var body: some View {
    VStack(alignment: .leading) {
        Picker("picker 타이틀", selection: $select) {
            ForEach(fruits, id: \.self) { fruit in
                Text(fruit)
            }
        }
        .pickerStyle(.inline)
        .background(.red)

        Picker(selection: $select, label: Text("Fruit")) {
            ForEach(fruits, id: \.self) {
                Text($0)
            }
        }
        .background(.green)

        NavigationView {
            Picker(selection: $select, label: Text("Fruit")) {
                ForEach(fruits, id: \.self) {
                    Text($0)
                }
            }
            .pickerStyle(.navigationLink)
            .background(Color.mint)
        }

        Picker("title", selection: $select) {
            ForEach(fruits, id: \.self) { fruit in
                Text(fruit)
            }
        }
        .pickerStyle(.segmented)
        .background(Color.orange)

    }.padding()
}


UIViewcontroller().present ->

View().sheet<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content : View
View().fullScreenCover<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content : View

struct ModalView: View {
    @Binding var isShow: Bool
    var body: some View {
        ZStack {
            Color.yellow.ignoresSafeArea()
            VStack {
                Button("close") {
                    isShow = false
                }
                .frame(height: 50)

                Text("hello world")
            }
            .background(.green)
        }
    }
}

struct TestPresent: View {
    @State private var fullShow = false
    @State private var modalShow = false

    var body: some View {

        VStack {
            Button("Present0") {
                self.fullShow = true
            }
            .padding()

            Button("Present1") {
                self.modalShow = true
            }
            .padding()

            .sheet(isPresented: $modalShow) {
                ModalView(isShow: $modalShow)
            } // 아직도 애네들이 어떻게 붙어서 동작하는지 이해가 안간다.
        }
        .fullScreenCover(isPresented: $fullShow) {
            ModalView(isShow: $fullShow)
        } // 도대체 넌 어떻게 붙어서 동작하는거니 안에 View 아무곳에 붙여도 동작함
    }
}

\

반응형

+ Recent posts