My app will have lists of images like shown in the below example:
To fetch an individual image from firebase, I defined a ViewModel class with the single image as a UIImage property. The appropriate view can then update with published changes to this ViewModel. With several--and in fact an arbitrary amount--of images in a list, I don't imagine that giving each one its own ViewModel will be efficient, especially because I imagine that I'll have to ForEach to loop through each image reference and create a ViewModel inside the parent view. This seems problematic because whenever the parent view is recreated I'm doing an O(n) operation to create and initialize view models for each image, not to mention the time it takes to re-fetch.
Is there a way to fetch multiple images from inside one ViewModel, storing them as [UIImage]? I would presumbably give this ViewModel an array of references. Alternatively, what architecture would you suggest to efficiently fetch any number of images from firebase given an array (of unknown size) of storage references?
I can provide more detail / code as needed.
This seems to work...
class MultipleImageFetcher: ObservableObject {
var firebaseManager: FirebaseManager
var imageDictionary: [String: UIImage] = [:]
#Published var isReady = false
init(_ firebaseManager: FirebaseManager) {
self.firebaseManager = firebaseManager
}
func fetchAllImages(references: [String]) {
for refID in references {
fetchIndividualImage(id: refID, totalNum:
references.count)
}
}
func fetchIndividualImage(id: String, totalNum: Int) {
let ref = getRefURL(uid: id)
ref.getData(maxSize: 2051240) { data, error in
if let error = error {
print("Error: \(error)")
return
}
self.imageDictionary[id] = UIImage(data: data!)
self.checkIsReady(num: totalNum)
}
}
func getRefURL(uid: String) -> StorageReference {
return firebaseManager.STORAGE.reference().child(uid)
}
func checkIsReady(num: Int) {
if imageDictionary.count == num {
self.isReady = true
} else {
self.isReady = false
}
}
}
struct ImageView: View {
#ObservedObject var fetcher: MultipleImageFetcher
init(_ firebaseManager: FirebaseManager) {
self.fetcher = MultipleImageFetcher(firebaseManager)
}
var body: some View {
Button {
fetcher.fetchAllImages(references:
["KXKFM94sk5OTyld2VsCQdhijd6m1", "MtgRaKMwyQfYX2uxuHs3iH3pLB52"])
} label: {
Text("Load Image")
}
if fetcher.isReady {
Text("Is Ready")
} else {
Text("Loading...")
}
}
}
Related
I want to load data from an API, then pass that data to several child views.
Here's a minimal example with one child view (DetailsView). I am getting this error:
Cannot convert value of type 'Binding<Subject>' to expected argument type 'BusinessDetails'
import Foundation
import SwiftUI
import Alamofire
struct BusinessView: View {
var shop: Business
class Observer : ObservableObject{
#Published public var shop = BusinessDetails()
#Published public var loading = false
init(){ shop = await getDetails(id: shop.id) }
func getDetails(id: String) async -> (BusinessDetails) {
let params = [
id: id
]
self.loading = true
self.shop = try await AF.request("https://api.com/details", parameters: params).serializingDecodable(BusinessDetails.self).value
self.loading = false
return self.shop
}
}
#StateObject var observed = Observer()
var body: some View {
if !observed.loading {
TabView {
DetailsView(shop: $observed.shop)
.tabItem {
Label("Details", systemImage: "")
}
}
}
}
}
This has worked before when the Observed object's property wasn't an object itself (like how the loading property doesn't cause an error).
When using async/await you should use the .task modifier and remove the object. The task will be started when the view appears, cancelled when it disappears and restarted when the id changes. This saves you a lot of effort trying to link async task lifecycle to object lifecycle. e.g.
struct BusinessView: View {
let shop: Business
#State var shopDetails = BusinessDetails()
#State var loading = false
var body: some View {
if loading {
Text("Loading")
}
else {
TabView {
DetailsView(shop: shopDetails)
.tabItem {
Label("Details", systemImage: "")
}
}
}
.task(id: shop.id) {
loading = true
shopDetails = await Self.getDetails(id: shop.id) // usually we have a try catch around this so we can show an error message
loading = false
}
}
// you can move this func somewhere else if you like
static func getDetails(id: String) async -> BusinessDetails{
let params = [
id: id
]
let result = try await AF.request("https://api.com/details", parameters: params).serializingDecodable(BusinessDetails.self).value
return result
}
}
}
I'm getting an object from Realm using #ObservedResults and then I need to modify the record in the same screen. Like adding items and deleting items, but I need this to happen without returning to previous NavigationView. And I need to update the view displaying the data as its updated.
It seems that #StateRealmObject should be the solution, but how can I designate a stored Realm object as a StateRealmObject? What would be a better approach?
Model:
class Order: Object, ObjectKeyIdentifiable {
#objc dynamic var orderID : String = NSUUID().uuidString
#objc dynamic var name : String = "No Name"
let orderLineId = List<OrderLine>()
.
.
.
override static func primaryKey() -> String? {
return "orderID"
}
}
View: (simplified)
struct OrderView: View {
var currentOrder : Order?
#ObservedResults (Order.self) var allOrders
var realm = try! Realm()
init(orderId: String) {
currentOrder = allOrders.filter("orderID ==\"\(orderId)\"")[0].thaw()
}
func deleteOrderLine(id: String, sku: String) {
try! realm.write {
let query = allOrderLines.filter("id == \"\(id)\" AND productSku == \"\(sku)\"")[0]
realm.delete(query)
}
}
var body: some View {
//Here is where all order data is displayed. When deleting a line. The view pops out. I //need to change data and keep view, and save changes as they are made.
//If delete line func is called, it works, but it pops the view.
deleteOrderLine(id: String, sku: String)
}
}
As per request, this is the parent navigation view, which I believe is the core of the problem.
struct OrderListView2: View {
#ObservedResults (Order.self) var allOrders
var body: some View {
VStack{
List{
ForEach(allOrders.sorted(byKeyPath: "dateCreated",ascending: false), id: \.self) { order in
NavigationLink(destination: OrderView(orderId: order.orderID)){
Text(order.info...)
}
}
The problem is that your allOrderLines collection is not frozen.
This result set is live so when you delete an entry your SwiftUI view is displaying a result set that is older than your live collection.
Hence the RLMException for the index out of bounds. To solve this problem you must freeze that collection for the view then delete the desired object and then reload your frozen collection. To achieve this i would suggest you use a viewmodel.
class OrderViewModel: ObservableObject {
#Published var orders: Results<Order>
init() {
let realm = try! Realm()
self.orders = realm.objects(Order.self).freeze()
}
func deleteOrderLine(id: String, sku: String) {
let realm = try! Realm()
try! realm.write {
let query = realm.objects(Order.self).filter("id == \"\(id)\" AND productSku == \"\(sku)\"")[0]
realm.delete(query)
}
self.orders = realm.objects(Order.self).freeze()
}
}
You can then use the #Published orders for your view. Initialize the viewmodel with #StateObject var viewModel = OrderViewModel()
I'm fetching books from an endpoint as such:
class APIManager: ObservableObject {
#Published var books = [Book]()
func fetchBooks() {
if let url = URL(string: urlEndpoint) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
if let safeData = data {
do {
let response = try JSONDecoder().decode([Book].self, from: safeData)
DispatchQueue.main.async {
self.books = response
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
My BookView looks like this:
struct BookView: View {
#ObservedObject var apiManager = APIManager()
var body: some View {
NavigationView {
List {
ForEach(apiManager.books) { book in
NavigationLink(
destination: BookDetailView(id: book.id, chapter: book.chapters),
label: {
HStack {
Text(book.name)
Spacer()
Text(book.testament)
}
})
}.navigationBarTitle("Book Title Here")
}.onAppear {
self.apiManager.fetchBooks()
}
}
}
}
When navigating to BookDetailView - I need to make another API call to fetch additional details about the book (such as chapters), given the book id that is passed here:
...
destination: BookDetailView(id: book.id, chapter: book.chapters)
...
Do I simply repeat the process and make another function in my APIManager class and add another #Published var chapters = [Chapter]()
And inside BookDetailView go
// Loop through each chapter here
// I want to display chapter details in this view
Text("You are viewing book id \(id). Chapter: \(chapter)").onAppear {
self.apiManager.fetchChapterDetails()
}
Doing so returns UIScrollView does not support multiple observers implementing
Whats the procedure here?
I would suggest making a separate ViewModel for the Detail Page. In that ViewModel you can pass in the id of the Book and make a separate API function. Also think about extracting the API Calls into a Service class, which being called from the ViewModel.
The Detail View and ViewModel could look like that...
class BookDetailViewModel: ObservableObject {
let bookId: Int
init(withBookId bookId: Int) {
self.bookId = bookId
}
func fetchBookInfos() {
// ...
}
}
struct BookDetailView: View {
#ObservedObject var viewModel: BookDetailViewModel
var body: some View {
NavigationView {
List {
Text("Book id \(viewModel.bookId)")
}.onAppear {
self.viewModel.fetchBookInfos()
}
}
}
}
When creating the Detail View, pass in the ViewModel.
I'm playing with SwiftUI and I'm currently struggling with images. Basically, I want to display an list of images with an infinite scroll but I want to keep the memory usage reasonable.
I have the following (truncated) code:
struct HomeView: View {
#State private var wallpapers: Loadable<[Wallpaper]> = .notRequested
#State private var currentPage = 1
#Environment(\.container) private var container
var body: some View {
content
.onAppear { loadWallpapers() }
}
private var content: some View {
VStack {
wallpapersList(data: wallpapers.value ?? [])
// ...
}
}
private func wallpapersList(data: [Wallpaper]) -> some View {
ScrollView {
LazyVStack(spacing: 5) {
ForEach(data) { w in
networkImage(url: w.thumbs.original)
.onAppear { loadNextPage(current: w.id) }
}
}
}
}
private func networkImage(url: String) -> some View {
// I use https://github.com/onevcat/Kingfisher to handle image loading
KFImage(URL(string: url))
// ...
}
private func loadWallpapers() {
container.interactors.wallpapers.load(data: $wallpapers, page: currentPage)
}
private func loadNextPage(current: String) {
// ...
}
}
struct WallpapersInteractor: PWallpapersInteractor {
let state: Store<AppState>
let agent: PWallpapersAgent
func load(data: LoadableSubject<[Wallpaper]>, page: Int) {
let store = CancelBag()
data.wrappedValue.setLoading(store: store)
Just.withErrorType((), Error.self)
.flatMap { _ in
agent.loadWallpapers(page: page) // network call here
}
.map { response in
response.data
}
.sink { subCompletion in
if case let .failure(error) = subCompletion {
data.wrappedValue.setFailed(error: error)
}
} receiveValue: {
if var currentWallpapers = data.wrappedValue.value {
currentWallpapers.append(contentsOf: $0) // /!\
data.wrappedValue.setLoaded(value: currentWallpapers)
} else {
data.wrappedValue.setLoaded(value: $0)
}
}
.store(in: store)
}
}
Because I append the new data to my Binding every time I request a new batch of images, the memory consumption quickly becomes stupidly high.
I tried to remove data from the array using .removeFirst(pageSize) once I get to the third page so that my array contains at most 2 * pageSize elements (pageSize being 64 in this case). But doing so makes my list all jumpy because the content goes up, which creates more problems than it solves.
I tried searching for a solution but I surprisingly didn't find anything on this particular topic, am I missing something obvious ?
I have a struct for my model and it needs to conform to a protocol that is NSObject only.
I am looking for a viable alternative to converting the model to a class. The requirements are:
Keeping the model a value type
Updating the model when photoLibraryDidChange is called
This would be the ideal implementation if PHPhotoLibraryChangeObserver would not require the implementation to be an NSObject
struct Model: PHPhotoLibraryChangeObserver {
var images:[UIImages] = []
fileprivate var allPhotos:PHFetchResult<PHAsset>?
mutating func photoLibraryDidChange(_ changeInstance: PHChange) {
let changeResults = changeInstance.changeDetails(for: allPhotos)
allPhotos = changeResults?.fetchResultAfterChanges
updateImages()
}
mutating func updateImages() {
// update self.images
...
}
}
I cannot pass the model to an external class implementing the observer protocol as then all the changes happen on the copy (its a value type...)
Any ideas? Best practices?
EDIT: Reformulated the question
EDIT 2: Progress
I have implemented a delegate as a reference type var of my model and pushed the data inside. Now photoLibraryDidChangeis not being called anymore.
This is the stripped down implementation:
class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver {
var allPhotos: PHFetchResult<PHAsset>?
var images:[UIImage] = []
override init(){
super.init()
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
if let changeResults = changeInstance.changeDetails(for: self.allPhotos!) {
self.allPhotos = changeResults.fetchResultAfterChanges
//this neve gets executed. It used to provide high quality images
self.updateImages()
}
}
}
func startFetching(){
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
PHPhotoLibrary.shared().register(self)
//this gets executed and fetches the thumbnails
self.updateImages()
}
fileprivate func appendImage(_ p: PHAsset) {
let pm = PHImageManager.default()
if p.mediaType == .image {
pm.requestImage(for: p, targetSize: CGSize(width: 1024, height: 768), contentMode: .default, options: nil){
image, _ in
if let im = image {
self.images.append(im)
}
}
}
}
fileprivate func updateImages() {
self.images = []
if let ap = allPhotos {
for index in 0..<min(ap.count, 10) {
let p = ap[index]
appendImage(p)
}
}
}
}
struct Model {
private var pkAdapter = PhotoKitAdapter()
var images:[UIImage] {
pkAdapter.images
}
func startFetching(){
pkAdapter.startFetching()
}
// example model implementation
mutating func select(_ image:UIImage){
// read directly from pkAdapter.images and change other local variables
}
}
I have put a breakpoint in photoLibraryDidChange and it just does not go there. I also checked that pkAdapter is always the same object and does not get reinitialised on "copy on change".
**EDIT: adding the model view **
This is the relevant part of the modelview responsible for the model management
class ModelView:ObservableObject {
#Published var model = Model()
init() {
self.model.startFetching()
}
var images:[UIImage] {
self.model.images
}
...
}
EDIT: solved the update problem
It was a bug in the simulator ... on a real device it works
I ended up with 2 possible designs:
decouple the PhotoKit interface completely from the model, let the modelview manage both and connect them, as the model view has access to the model instance.
create a PhotoKit interface as var of the model and push the mutable data that is generated by the PhotoKit interface inside it, so it can be changed with an escaping closure. The model is never called from the interface but just exposes the data inside the PhotoKit through a computer property.
I will show two sample implementation below. They are naive in many respect, they ignore performance problems by refreshing all pictures every time the PhotoLibrary is updated. Implementing proper delta updates (and other optimisation) would just clutter up the code and offer no extra insight on the solution to the original problem.
Decouple the PhotoKit interface
ModelView
class ModelView:ObservableObject {
var pkSubscription:AnyCancellable?
private var pkAdapter = PhotoKitAdapter()
#Published var model = Model()
init() {
pkSubscription = self.pkAdapter.objectWillChange.sink{ _ in
DispatchQueue.main.async {
self.model.reset()
for img in self.pkAdapter.images {
self.model.append(uiimage: img)
}
}
}
self.pkAdapter.startFetching()
}
}
Model
struct Model {
private(set) var images:[UIImage] = []
mutating func append(uiimage:UIImage){
images.append(uiimage)
}
mutating func reset(){
images = []
}
}
PhotoKit interface
class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver, ObservableObject {
var allPhotos: PHFetchResult<PHAsset>?
var images:[UIImage] = []
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
if let changeResults = changeInstance.changeDetails(for: self.allPhotos!) {
self.allPhotos = changeResults.fetchResultAfterChanges
self.updateImages()
self.objectWillChange.send()
}
}
}
func startFetching(){
PHPhotoLibrary.requestAuthorization{status in
if status == .authorized {
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
self.allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
PHPhotoLibrary.shared().register(self)
self.updateImages()
}
}
}
fileprivate func appendImage(_ p: PHAsset) {
// This actually appends multiple copies of the image because
// it gets called multiple times for the same asset.
// Proper tracking of the asset needs to be implemented
let pm = PHImageManager.default()
if p.mediaType == .image {
pm.requestImage(for: p, targetSize: CGSize(width: 1024, height: 768), contentMode: .default, options: nil){
image, _ in
if let im = image {
self.images.append(im)
self.objectWillChange.send()
}
}
}
}
fileprivate func updateImages() {
self.images = []
if let ap = allPhotos {
for index in 0..<min(ap.count, 10) {
let p = ap[index]
appendImage(p)
}
}
}
PhotoKit interface as property of the model
ModelView
class ModelView:ObservableObject {
#Published var model = Model()
}
Model
struct Model {
private var pkAdapter = PhotoKitAdapter()
var images:[UIImage] { pkAdapter.images }
}
PhotoKit interface
class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver{
var allPhotos: PHFetchResult<PHAsset>?
var images:[UIImage] = []
override init(){
super.init()
startFetching()
}
// the rest of the implementation is the same as before
...