KoreaMango 나무

[iOS App Dev Tutorials] SwiftUI - Persistence and Concurrency (1) 본문

iOS/iOS App Dev Tutorials

[iOS App Dev Tutorials] SwiftUI - Persistence and Concurrency (1)

KoreaMango 2022. 5. 18. 21:24
 

Apple Developer Documentation

 

developer.apple.com

1. Persisting Data

이번 장에서는 scrum을 Load하고 Save하기 위한 메소드와 앱의 모델을 위해 Codable을 추가할 것이다.

Section 1. Add Codable Conformance

이 섹션에서 Model에 Codable을 추가할 것이다.

Codable이란 Encodable 과 Decodable을 결합한 type 별칭이다. 이 프로토콜을 사용해서 구현할 때, Codable API는 JSON 파일을 구현화하기에 더 쉽게 만들어준다.

enum Theme: String, CaseIterable, Identifiable, Codable { ... }

struct History: Identifiable, Codable { ... }

struct DailyScrum: Identifiable, Codable {
    struct Attendee: Identifiable, Codable { ... }
}

이렇게 Model 파일에 Codable 프로토콜을 추가한다.

Section 2. Create a Data Store

앱의 데이터 모델로 사용할 새로운 클래스를 만든다.

import SwiftUI

class ScrumStore: ObservableObject {
	@Published var scrums: [DailyScrum] = []
}

ObservableObject 를 준수하는 클래스를 만든다. ObservableObject 는 외부 모델 데이터와 SwiftUI view 들과 연결해주기 위한 클래스 제한 프로토콜이다.

ObservableObject 는 @Published 프로퍼티 중 하나가 변경되려고 할 때 내보내는 objectWillChange publisher가 포함되어 있다. ScrumStore의 인스턴스를 관찰하는 모든 view는 스크럼 값이 변경될 때 다시 렌더링 된다.

private static func fileURL() throws -> URL {
	try FileManager.default.url(for: .documentDirectory,
                                       in: .userDomainMask,
                                       appropriateFor: nil,
                                       create: false)
	  .appendingPathComponent("scrums.data")

}

사용자의 문서 폴더에 저장하고 로드를 쉽게 접근하기 위한 func 이다.

FileManager 는 현재 사용자의 문서 위치를 가져오기 위한 클래스이다.

appendingPathComponent(_:) 를 사용해서 “scrums.data” 라는 이름의 파일의 url을 반환한다.

Section 3. Add a Method to Load Data

“scrums.data”라는 파일로부터 데이터를 가져와 scrum 배열을 채우는 함수를 만든다.

static func load(completion: @escaping (Result<[DailyScrum], Error>)->Void) {
	DispatchQueue.global(qos: .background).async {
	do {
		let fileURL = try fileURL()
				guard let file = try? FileHandle(forReadingFrom: fileURL) else {
		      DispatchQueue.main.async {
		          completion(.success([]))
		      }
		      return
				}
				let dailyScrums = try JSONDecoder()
													.decode([DailyScrum].self, from: file.availableData)
				DispatchQueue.main.async {
            completion(.success(dailyScrums))
        }  
		} catch {
				DispatchQueue.main.async {
            completion(.failure(error))
        }
	  }
	}
}

load function은 배열 또는 에러를 비동기적으로 호출하는 컴플리션 클로져를 받아들인다.

DispatchQueue는 앱이 작업을 제출할 수 있는 FIFO Queue 이다. background는 모든 태스크 중 가장 우선순위가 낮다. do- catch 구문을 사용해서 데이터 로드 에러를 잡을 것이다.

fileURL을 위한 지역 상수를 생성하고 scrums.data유ㅈ 읽는 파일 핸들을 만든다.

사용자가 앱을 처음 시작할 때 scrums.data가 없으므로 파일 핸들을 여는 동안 오류가 발생하면 핀 배열로 완료 핸들러를 호출한다.

guard let 을 통과하면 dailyScrums 에 디코딩한다. 그리고 메인 큐에서 디코딩이 완료된 스크럼을 컴플리션 핸들러로 전달한다. 백그라운드에서 파일을 열고 디코딩하는 것은 시간이 더 오래 걸린다. 그리고 작업이 완료되면 메인 큐로 돌아온다.

문제를 발견했을 때는 error를 컴플리션 핸들러에 전달한다.

Section 4. Add a Method to Save Data

사용자의 파일시스템의 스크럼에 저장하기 위해 메소드를 작성해본다.

static func save(scrums: [DailyScrum], completion: @escaping (Result<Int, Error>)->Void) {
	DispatchQueue.global(qos: .background).async {
      do {
				let data = try JSONEncoder().encode(scrums)
				let outfile = try fileURL()
				try data.write(to: outfile)
				DispatchQueue.main.async {
            completion(.success(scrums.count))
        }
      } catch {
				DispatchQueue.main.async {
            completion(.failure(error))
        }
      }
  }
}

이 함수는 저장된 스크럼 수 또는 오류를 허용하는 완료 핸들러를 받아들인다.

백그라운드 큐에 do-catch 구문을 생성한다.

매개변수로 받아온 scrums을 Encode 하고 상수에 넣는다. 그리고 fileURL을 사용해 위치를 얻는다.

위치에다가 data를 write 한다. 성공했을 때에는 count를 컴플리션 핸들러로 보내고 실패하면 error를 내보낸다.

Section 5. Load Data on App Launch

이번 섹션에서는 App의 root View가 화면에 나타날 때 데이터를 Load 하는 방법을 알아볼 것이다.

@main
struct ScrumdingerApp: App {
    @StateObject private var store = ScrumStore()
    
    var body: some Scene {
        WindowGroup {
            NavigationView {
                ScrumsView(scrums: $store.scrums)
            }
            .onAppear {
                ScrumStore.load { result in
                    switch result {
                    case .failure(let error):
                        fatalError(error.localizedDescription)
                    case .success(let scrums):
                        store.scrums = scrums
                    }
                }
            }
        }
    }
}

@StateObject property wrapper 는 이를 관찰하는 구조의 각 인스턴스에 대해 관찰 가능한 단일 인스턴스를 만든다.

NavigationView가 나타날 때 데이터를 로드하기 위해서 NavigationView 아래에 onAppear을 추가한다.

result가 성공일 때와 실패일 때 케이스를 나눠서 코드를 작성한다.

Section 6. Save Data in Inactive State

비활성화 상태에서 데이터를 저장하는 방법에 대해 알아보자.

@Environment(\\.scenePhase) private var scenePhase

SwiftUI는 앱의 Scene 인스턴스의 현재 작동 상태를 scenePhase Environment Value로 나타낸다.

이 값을 관찰하고 비활성 상태가 되면 사용자 데이터를 저장한다.

let saveAction: ()->Void

saveAction을 추가하고 preview에 추가해준다. 이 클로저는 ScrumsView가 인스턴스화될 때 제공된다.

.onChange(of: scenePhase) { phase in
    if phase == .inactive { saveAction() }
}

ScenePhase 값을 관찰하는 onChange 을 추가한다. Scene이 비활성화 단계로 이동하는 경우 saveAction 을 호출한다. 비활성 단계의 Scene은 더 이상 이벤트를 수신하지 않으며 사용자가 사용하지 못할 수 있다.

ScrumsView(scrums: $store.scrums) {
      ScrumStore.save(scrums: store.scrums) { result in
					if case .failure(let error) = result {
              fatalError(error.localizedDescription)
          }
      }
  }

ScrumsView에서 scrumStore를 불러올 때 후행 클로져를 사용해서 saveAction을 만들었다.

ScrumStore의 save 메소드가 정상적으로 실행되면 매개변수에 있는 scrums에 제대로 들어갈 것이고 에러가 발생하면 에러를 출력할 것 이다.