Concurrent Data models
struct SpacePhoto {
var title: String
var description: String
var date: Date
var url: URL
...
}
struct SpacePhoto: Codable {}
struct SpacePhoto: Identifiable {}
class Photos: ObservableObject {
@Published var items: [SpacePhoto]
func updateItems() {
// TO-DO:
}
}
struct CatalogView: View {
@StateObject private var photos = Photos()
var body: some View {
NavigationView {
List {
ForEach(photos.items) { item in
PhotoView(photo: item)
.listRowSeparator(.hidden)
}
}
.navigationTitle("Catalog")
.listStyle(.plain)
}
}
}
struct PhotoView: View {
var photo: SpacePhoto
var body: some View {
ZStack(alignment: .bottom) {
HStack {
Text(photo.title)
Spacer()
}
.padding()
.background(.thinMaterial)
}
.background(.thickMaterial)
.mask(RoundedRectangle(cornerRadius: 16))
.padding(.bottom, 8)
}
}
func updateItems() {
let fetched = /* 새로운 아이템 가져옴 */
items = fetched // objectWillChange 이벤트 발생. 이때 snapshot이 만들어지고
// 이 스냅샷을 가지고 기존 데이터와 diff를 수행해서 업데이트
}
이때 메인 액터에서 너무 많은 일을 하면 업데이트가 느려지고, 사용자 이벤트를 받지 못하게 된다.
업데이트 틱을 놓치게 되고, 사용자에게는 hitch로 보이게 된다.
그렇다고 업데이트를 메인 액터가 아닌곳에서 하면, 런루프 틱이 발생할 때, 상태 변화가 없을 수 있다.
func updateItems() {
let fetched = fetchPhotos()
items = fetched
}
objectWillChange, 상태 업데이트, 런루프 틱은 항상 이 순서대로 일어나야만 한다.
메인 액터에서 이를 수행하면, 이를 보장할 수 있다. 그래서 DispatchQueue.main을 사용했다.
이제는 await를 쓰면 된다! 작업을 메인 액터에 yield 시킨다.
func updateItems() async {
let fetched = await fetchPhotos()
items = fetched
}
// task group을 쓰면 더 효율적이지만 여기서는 샘플이니까
func fetchPhotos() await -> [SpacePhoto] {
var downloaded: [SpacePhoto] = []
for date in randomPhotoDates() {
let url = SpacePhoto.requestFor(date: date)
if let photo = await fetchPhoto(from: url) {
downloaded.append(photo)
}
}
return downloaded
}
func fetchPhoto(from url: URL) async -> SpacePhoto {
do {
let (data, _) = try await URLSession.shared.data(from: url)
return try SpacePhoto(data: data)
} catch {
return nil
}
}
클래스에 @MainActor를 명시하면 해당 클래스의 모든 메소드들은 MainActor에서 돌아간다.
@MainActor
class Photos: ObservableObject {
View에서는 .task modifier로 호출한다. → onAppear에서 하던 비동기 호출을 여기서 하면 된다.
struct CatalogView: View {
@StateObject private var photos = Photos()
var body: some View {
NavigationView {
List {
ForEach(photos.items) { item in
PhotoView(photo: item)
.listRowSeparator(.hidden)
}
}
.navigationTitle("Catalog")
.listStyle(.plain)
}
.task {
await photos.updateItems()
}
}
}
이미지는 AsyncImage로 호출한다.
struct PhotoView: View {
var photo: SpacePhoto
var body: some View {
ZStack(alignment: .bottom) {
AsyncImage(url: photo.url) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
progressView()
}.frame(minWidth: 0, minHeight: 400)
HStack {
Text(photo.title)
Spacer()
SavePhotoButton(photo: photo)
}
.padding()
.background(.thinMaterial)
}
.background(.thickMaterial)
.mask(RoundedRectangle(cornerRadius: 16))
.padding(.bottom, 8)
}
}
저장 버튼 구현
struct SavePhotoButton: View {
var photo: SpacePhoto
@State private var isSaving = false
var body: some View {
Button {
async {
isSaving = true
await photo.save()
isSaving = false
}
} label: {
Text("Save")
.opacity(isSaving ? 0 : 1)
.overlay { // localization에 따라서 뷰 크기가 유동적으로 조절되도록
if isSaving {
ProgressView()
}
}
}
.disabled(isSaving)
.buttonStyle(.bordered)
}
}
리프레시 기능 구현
struct CatalogView: View {
@StateObject private var photos = Photos()
var body: some View {
NavigationView {
List {
ForEach(photos.items) { item in
PhotoView(photo: item)
.listRowSeparator(.hidden)
}
}
.navigationTitle("Catalog")
.listStyle(.plain)
.refreshable {
await photos.updateItems()
}
}
.task {
await photos.updateItems()
}
}
}