Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- RxCocoa
- SWIFT
- swift documentation
- rxswift
- clean architecture
- 애니메이션
- UITextView
- uitableview
- Xcode
- 리펙터링
- combine
- Observable
- ios
- 클린 코드
- map
- MVVM
- 리팩토링
- UICollectionView
- ribs
- Clean Code
- 스위프트
- Protocol
- swiftUI
- Refactoring
- uiscrollview
- tableView
- Human interface guide
- collectionview
- HIG
- 리펙토링
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - SwiftUI] 2. Pagination 방법 (페이지네이션, Combine) - 최적화, 이미지 캐싱 본문
iOS 응용 (SwiftUI)
[iOS - SwiftUI] 2. Pagination 방법 (페이지네이션, Combine) - 최적화, 이미지 캐싱
jake-kim 2022. 10. 6. 22:581. Pagination 방법 (페이지네이션, Combine) - 기초
2. Pagination 방법 (페이지네이션, Combine) - 메인 스레드 최적화, 이미지 캐싱
이미지를 불러올때 고려할 것
- url을 가지고 이미지를 불러올 때, main thread에서 이미지를 가져오면 앱이 버벅이는 현상이 존재
- 심지어 AsyncImage를 통해서 async하게 이미지를 불러와도 앱이 버벅이는 현상이 존재
- background thread에서 url을 통해 이미지를 불러오도록 구현
- Image Caching 적용을 통해 조금 더 빠른 로딩이 되도록 구현
- 이미지 캐싱이 없다면 매번 url을 네트워킹을 통해서 이미지를 획득하므로 캐싱이 필수
구현 아이디어
- url을 통해 이미지를 불러오는 코드는 background thread에서 동작하도록 구현
- viewModel에서 url 배열이 있을때 이 배열들을 모두 순회하며 uiImage 형태로 모두 로드하여 한꺼번에 View쪽에 넘기는 형태로 구현
- View에서 url을 가지고 로드하게되면 아무리 캐싱을 사용한다해도 계속 캐시를 확인하고 로드하는 과정이 있게되어 퍼포먼스 저하를 대비
- 이미지 캐싱은 NSCache(NSString, UIImage)()을 이용하여 구현
- 이미지 관련 Kingfisher, SDWebImage와 같은 것들을 사용해도 내부적으로 url을 가지고 이미지를 가져올때도 메인 스레드에서 동작하므로 직접 구현할것
구현
- 사용 API - 이미지 리스트를 불러오는 Flickr
- (예제를 위해서 페이지네이션이 있다고 가정하고 구현)
https://api.flickr.com/services/feeds/photos_public.gne?tags=texas&tagmode=any&format=json&nojsoncallback=1
- API Decoding 모델 정의
struct PhotoModel: Codable {
struct Item: Codable {
struct Media: Codable {
let m: String
}
let media: Media
let description: String
}
let items: [Item]
}
extension PhotoModel {
var url: String? {
items.first?.media.m
}
var photoUrlStrings: [String] {
items.map(\.media.m)
}
var coreItems: [(String, String)] {
var res = [(String, String)]()
for i in 0..<items.count {
res.append((items[i].media.m, items[i].description))
}
return res
}
}
- UI에 표출될때 사용될 전용 Model 정의
- 뷰쪽에서 urlString이 아닌 UIImage 그대로 사용할것이므로 모델에 UIImage 프로퍼티 선언
- ForEach를 사용하여 표출할것이므로 Identifiable 준수
- 이미지를 표출할때 비율을 알아야하므로 width, height 필요
struct Photo: Identifiable {
let url: String
let uiImage: UIImage
let width: CGFloat
let height: CGFloat
}
extension Photo {
var id: String { url }
}
extension Photo: Hashable {}
- View, ViewModel 선언
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel = ContentViewModel()
private var photoWidth: CGFloat {
UIScreen.main.bounds.width
}
init() {
viewModel.fetchPhoto()
}
var body: some View {
NavigationView {
getList()
.navigationTitle("사진")
}
}
@ViewBuilder
private func getList() -> some View {
// TODO
}
}
final class ContentViewModel: ObservableObject {
@Published var photos = [Photo]()
private var page = 0
func fetchPhoto() {
//TODO
}
}
- View의 getList 구현
- tip) SwiftUI에서 제공하는 List, GridView 같은 것을 사용하지 않고 ScrollView, LazyVStack, ForEach, NavigationLink를 사용하여 구현해야 커스텀이 편리
- 페이지네이션은 ForEach 마지막에 ProgressView를 추가하고 여기에 onAppear시 viewModel에 데이터를 가져와달라고 요청
@ViewBuilder
private func getList() -> some View {
ScrollView {
LazyVStack {
ForEach(viewModel.photos) { photo in
NavigationLink(
destination: {
Image(uiImage: photo.uiImage)
.resizable()
.frame(
width: photoWidth,
height: getHeight(imageWidth: photo.width, imageHeight: photo.height)
)
},
label: {
Image(uiImage: photo.uiImage)
.resizable()
.frame(
width: photoWidth,
height: getHeight(imageWidth: photo.width, imageHeight: photo.height)
)
}
)
}
if !viewModel.photos.isEmpty {
HStack {
Spacer()
ProgressView()
.onAppear {
viewModel.fetchPhoto()
}
Spacer()
}
}
}
}
}
private func getHeight(imageWidth: Double, imageHeight: Double) -> Double {
photoWidth * imageHeight / imageWidth
}
- viewModel 의 fetchPhoto 구현
- ImageCache를 사용하여 중간중간 urlString에 대한 UIImage 캐싱
- 해당 API는 width와 height값이 문자열로 주어지므로, getWidthHeight라는 메소드를 통해 가져오도록 구현
func fetchPhoto() {
page += 1
DispatchQueue.global().async {
guard
let url = URL(string: "https://api.flickr.com/services/feeds/photos_public.gne?tags=texas&tagmode=any&format=json&nojsoncallback=1")
else { return }
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard
let ss = self,
let data = data,
let photoModel = try? JSONDecoder().decode(PhotoModel.self, from: data)
else { return }
var newPhotos = [Photo]()
photoModel
.coreItems
.forEach { urlString, description in
let widthHeight = ss.getWidthHeight(description: description)
if let uiImage = ImageCache.shared.object(forKey: urlString as NSString) {
newPhotos.append(.init(url: urlString, uiImage: uiImage, width: widthHeight.0, height: widthHeight.1))
} else {
guard
let url = URL(string: urlString),
let data = try? Data(contentsOf: url),
let uiImage = UIImage(data: data)
else { return }
ImageCache.shared.setObject(uiImage, forKey: urlString as NSString)
newPhotos.append(.init(url: urlString, uiImage: uiImage, width: widthHeight.0, height: widthHeight.1))
}
}
DispatchQueue.main.async {
ss.photos = ss.photos + newPhotos
}
}.resume()
}
}
private func getWidthHeight(description: String) -> (CGFloat, CGFloat) {
let array = description.split(separator: " ").map(String.init)
let widthStr = array
.first(where: { $0.prefix(5) == "width" })!
.replacingOccurrences(of: "\"", with: "")
.replacingOccurrences(of: "width=", with: "")
let heightStr = array
.first(where: { $0.prefix(6) == "height" })!
.replacingOccurrences(of: "\"", with: "")
.replacingOccurrences(of: "height=", with: "")
return (Double(widthStr)!, Double(heightStr)!)
}
완성)
'iOS 응용 (SwiftUI)' 카테고리의 다른 글
Comments