SDWebImageSwiftUI - Image Loading and Caching - ios

I'm working on developing an image heavy iOS app:
Users upload images to the app, all of which are then added to a cloud storage bucket. A database entry is also created, which contains the url to the image's location in the cloud storage bucket.
The SDWebImageSwiftUI framework is used to load the user's images (that are in the cloud storage bucket) into the app and display them.
When a user signs into their account or when a user launches the app and they are already authenticated, a "loading" screen appears while the rest of their account information is retrieved from a database. Part of the information that is collected from the database includes a url to each of the user's uploaded images. Once the app receives these urls (and while the "loading" screen is still on display), I load in the images. Once all the information has been received from the database and once all the images are preloaded, the "loading" screen disappears, taking the user into the app, where the images are displayed right away. I set it up this way so that when the user is taken into the "normal" app screens, they don't have to wait again for the images to load and populate into the view. Below you will find the code that I use to preload these images.
import Foundation
import SDWebImageSwiftUI
// used to retrieve and collect all information relating to a user and their account
class SessionStore: ObservableObject {
#Published var user: User?
#Published var collections: [Collection]?
// ...
// called after a user's collections have been retrieved from the database
func preloadImages() {
guard let collections = self.collections else { return }
// each collection object / item has an image associated with it, so iterate through all of the user's collections
for collection in collections {
// load the collection's image
let imageManager = ImageManager(url: URL(string: collection.imageUrl))
imageManager.load()
}
}
}
The SDWebImageSwiftUI framework has caching capabilities, but I am not currently taking advantage of them as I don't feel confident in my understanding of the subject and cannot find much information online. I am hoping that someone will be able to please explain this more in depth to me and potentially give me some recommendations? Some of my questions are detailed below:
Should I use a cache in my app since it so heavily relies on images?
How can I set up and use SDWebImageSwiftUI's cache systems? What are some good guidelines / principles for configuring a cache system (i.e., memory and disk size)?
When does a SDWebImageSwiftUI cache system reset, if ever?
If I add a cache system, would that help images load faster? Would I be able to remove the image "preloading" functionality I detailed above?
Additionally, I've noticed that if the app moves to the background and then later reenters the foreground, the images are being reloaded. So any images that were being displayed on the user's last screen, temporarily disappear when the app first opens back up. In this case, the "preloading" functionality that I detailed above is not triggered to run. Rather, the images are loaded in due to their presence in the view (see below code), meaning that only the images on the one screen are loaded back in. If I then navigate to another app screen, the images again won't be preloaded and will load as / if they are presented in the view.
import SwiftUI
import SDWebImageSwiftUI
struct ExampleView: View {
#EnvironmentObject var session: SessionStore
let threeColumnGrid = [GridItem(.fixed(UIScreen.main.bounds.width * 0.28)), GridItem(.fixed(UIScreen.main.bounds.width * 0.28)), GridItem(.fixed(UIScreen.main.bounds.width * 0.28))]
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: threeColumnGrid, alignment: .center, spacing: 10) {
ForEach(session.collections ?? [], id: \.collectionId) { (collectionItem) in
// when the app enters the foreground after being in the background, the images are no longer preloaded into the app
// so instead of seeing an image displayed within a rounded rectangle (as expected), the user will see an empty rounded rectangle as the image reloads
// the following line is what seems to be triggering the image to be loaded again
WebImage(url: URL(string: collectionItem.imageUrl))
.resizable()
.scaledToFit()
.padding(.vertical, 5)
.padding(.horizontal, 10)
.frame(width: UIScreen.main.bounds.width * 0.28, height: UIScreen.main.bounds.height * 0.22)
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.black.opacity(0.5), lineWidth: 1)
)
} // ForEach
} // LazyVGrid
.padding(.top, 2)
} // ScrollView
}
}
I am also wondering if a cache would help resolve these image "loading lags" when the app renters the foreground? As I believe the cache would have a loaded version of the image, which can be accessed and displayed quicker?
Any knowledge on SDWebImageSwiftUI's caching capabilities, would be greatly appreciated!

Related

iOS 16 Lock-screen Widget: can we add a deep link to the widget

I want the user to be able to tap on a lock-screen widget created by my app and deep link to my main app (eg, so that my main app can take some action)
I have been able to do this with Home Widgets, but the same method does not work with the Lock Screen widgets in iOS 16.
Is this possible?
Here is a code that works for Home Widgets:
Link(destination: myActionURL) {
Image("my_image").resizable(capInsets: EdgeInsets(), resizingMode: .stretch)
I was able to do this using widgetURL. In the app delegate catch the url using func applicationHandle(url: URL) and do the action based on the url.
VStack(spacing: 0) {
Text("\(String(carName))").font(.system(size: 10)).fontWeight(.bold).minimumScaleFactor(0.4)
Image("action").resizable(capInsets: EdgeInsets(), resizingMode: .stretch)
.aspectRatio(contentMode: .fit).foregroundColor(entry.widgetTextColor)
}
.widgetURL(url)
It is my understanding that lock screen widgets are not interactive. They’re meant to show glanceable content from your app, not have tappable elements.
If you find out anything that contradicts this, I would love to hear it as I also wish to have them be tappable.

PDFView causes app to freeze on iOS 16, only when autoscales = true

I am using a PDFView to display images in my app (built using SwifUI), simply for quick and easy pinch-to-zoom functionality. This worked perfectly in iOS 15, but since updating to iOS 16, the app freezes when attempting to load the image viewer (PhotoDetailView below). The issue persists across both the simulator and a physical device.
Here is the code I'm using:
import SwiftUI
import PDFKit
struct PhotoDetailView: UIViewRepresentable {
let image: UIImage
func makeUIView(context: Context) -> PDFView {
let view = PDFView()
view.document = PDFDocument()
guard let page = PDFPage(image: image) else { return view }
view.document?.insert(page, at: 0)
view.autoScales = true
return view
}
func updateUIView(_ uiView: PDFView, context: Context) {
// empty
}
}
When I run the code on iOS 16.0 in the simulator, I get 2 errors in the console:
[Assert] -[UIScrollView _clampedZoomScale:allowRubberbanding:]: Must be called with non-zero scale
[Unknown process name] CGAffineTransformInvert: singular matrix.
I have been able to isolate the issue to view.autoscales = true. If I print view.scaleFactor, I can see that it is 1.0 before the autoscale and 0.0 afterward (which is what appears to be prompting the errors). These errors also show up in the console when using iOS 15 in the simulator, but the images load as expected.
When view.autoscales = true is commented out, the image loads, albeit at a size that is much larger than the device screen.
Does anyone have any idea what may be causing this? I'd really like to avoid having to build a custom viewer, since I'm just trying to let users quickly pinch to zoom on images.
I managed to resolve this issue. I'm posting my solution here, in case anyone else runs into the same type of problem.
The issue only occurred when using a NavigationLink to navigate to PhotoDetailView. This led me to look more closely at my navigation stack.
After some digging, I found that the problem was related to my use of .navigationViewStyle(.stack) on the NavigationView. I needed this modifier to get things to display correctly on iOS 15, but the same modifier was breaking my image viewer with iOS 16.
My solution was to create a custom container to replace NavigationView, which conditionally uses NavigationStack for iOS 16, or NavigationView + .navigationViewStyle(.stack) for iOS 15. That worked like a charm and my image viewer is back in action.
I found inspiration for my custom container here: https://developer.apple.com/forums/thread/710377

Apple's SwiftUI tutorial code doesn't display view contents in homemade copy

I'm just getting started learning SwiftUI, following Apple's tutorial for building an iOS app at this link. I started with the skeleton app provided and have followed along in the latest Xcode, step by step. Instead of seeing what I am supposed to see, the CardView displays only a blank white rectangle.Apple provided a fully built project, which works just fine. I've compared the code I built from the skeleton project to the code that comes with the completed project, and they appear identical. Yet my home-built copy of the project simply does not fill in any details in CardView. No color, text, nor icons, just a blank white rectangle. Other than simply comparing the code to the canned example, I have utterly no idea how to debug this. Any suggestions are welcome.
Here's the code of CardView.swift:
import SwiftUI
struct CardView: View {
let scrum: DailyScrum
var body: some View {
VStack(alignment: .leading) {
Text(scrum.title).font(.headline)
Spacer()
HStack {
Label("\(scrum.attendees.count)", systemImage: "person.3")
.accessibilityElement(children: .ignore)
.accessibilityLabel(Text("Attendees"))
.accessibilityValue(Text("\(scrum.attendees.count)"))
Spacer()
Label("\(scrum.lengthInMinutes)", systemImage: "clock")
.padding(.trailing, 20)
.accessibilityElement(children: .ignore)
.accessibilityLabel(Text("Meeting length"))
.accessibilityValue(Text("\(scrum.lengthInMinutes) minutes"))
}
.font(.caption)
}
.padding()
.foregroundColor(scrum.color.accessibleFontColor)
}
}
struct CardView_Previews: PreviewProvider {
static var scrum = DailyScrum.data[0]
static var previews: some View {
CardView(scrum: scrum)
.background(scrum.color)
.previewLayout(.fixed(width: 400, height: 60))
}
}
This is the image I see from my code in Xcode's preview:
And here is the one I see in Xcode's preview from Apple's already-complete sample code:
It seems reasonable that comparing the code I built to the code provided by Apple would highlight the problem, but zero differences come up. The two other views in the project work fine. Are there any other modules that would be significant besides this source, CardView.swift?
I ran into the same problem and found that I missed Step 5 in the "Creating a Card View -> Section 1: Create a Color Theme" part of the tutorial
I don't think the instructions in this step very clear here for someone not familiar with Xcode. I solved this by importing the resources from a downloaded version of the sample project. Try these steps
Open the Assets file in Xcode
Right click and select Import
Navigate to the downloaded project and select the "Themes" directory, which is a subdirectory of Assets.xcassets
Your Assets file should look like this when you're finished. As a bonus you can use these same steps to import the app icon.
Understand the issue
scrum.color.accessibleFontColor applied to the foregroundColor of the card. So all labels and images and texts and etc. will be affected.
accessibleFontColor seems to return either black or white according to the parent color (scrum.color in this case)
seems Color("Design") that you are passing for the preview is returning clear color because there is no color with the name Design in assets in the starting project. So the preview shows the default white color as the card background. But it also uses the white color for the foreground. So you see a solid white card in the result.
Solutions
Comment out the .foregroundColor(scrum.color.accessibleFontColor)
Or add an assets color and name it as expected.
Or give the card a manual background
Or download the completed version and reverse the process to the point that you are at.
I downloaded the project file from the tutorial, and I found there's a file that you probably missed. You can check the Models folder in the sample code, there's a file called Color+Codable.swift which is responsible for this accessibleFontColor thingy.
This is caused by undefined "Design" color.
Copy the "Design" color from Assets.xcassets from Apple's sample project, and put the color's definition under the Assets.xcassets directory of your project can solve your problem.

Swift: Has anyone had code work properly on their device, but not in simulator?

I was building an application at the beginning of this year up until around March, and then I took a break and recently wanted to pick it up again. However, my code was behaving differently from when I last used it. I had not made any changes to the code, and strangely even past versions from months prior also weren't working, and those certainly weren't changed.
From some troubleshooting here are things that worked based on this code:
Here is the main ContentView class
var body: some View {
return ZStack {
setColor
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
VStack(alignment:.trailing){
VStack{
DisplayBar(majorKeyManager:majorKeyManager)
}.zIndex(2.0)
VStack{
GuitarView(guitarModel:guitarModel)//.position(x:556, y:220)
}.padding(.bottom, 20)
VStack{
PianoView(pianoModel: pianoModel)//majorKeyManager: majorKeyManager, audioEngine: audioEngine)//.position(x:556,y:600)
}
}
}.edgesIgnoringSafeArea(.top)//.padding(10)
}
Then here is the GuitarView
struct GuitarView: View {
#ObservedObject var guitarModel:GuitarModel
var body: some View {
var needsMarker = false
var needsIndexMarker = false
var needsCapo = false
return HStack (spacing:0){
VStack{
ForEach(0..<guitarModel.numStrings){ stringIndex in
VStack{
Here is the PianoView
struct PianoView: View {
#ObservedObject var pianoModel:PianoModel
var body: some View {
return VStack(alignment:.leading){
ZStack{
HStack (spacing:2){ // DRAW WHITE KEYS
ForEach(0..<self.pianoModel.whiteKeyMap.count){ (keyIndex) -> KeyButtonView in
I didn't think it was important to show the details of what is happening within GuitarView/PianoView. They are structured very similarly, except the difference is the PianoView always behaves properly, while the GuitarView is hit or miss. I placed a red line in the GuitarView image that shows where it is not entering the ForEach, yet it is meeting the conditions of the ForEach (the variable it is checking is always 12).
Both views are being updated based on their respective models which are ObservedObjects. The model is correctly being updated for both the Guitar and Piano. However, GuitarView gets called once when the initial program is launched, but is sometimes never called again. However, this varies depending on whether I'm running it within device simulator, versus a real device and which XCode version I am using.
On XCode 11.4.1, the code does not work properly on the device simulator on any device (Mac, iPhone, iPad, etc.). However, it does run properly on real devices like an iPhone and an iPad. I tried reinstalling XCode but that changed nothing.
Then, I tried rolling back my version of XCode to 11.2.1, and the code runs properly in the iPad simulator, but not as a Mac simulator.
Has anyone had any similar problems to this? I can't seem to figure out why this code gets called once, but then never gets called again despite its ObservedObject being clearly updated, and another nearly identical view (PianoView), is clearly working.

Unable to setup images from JSON?

I'm using Firebase Database & Firebase Storage. I have an iOS Swift app, that allows the user to select multiple images, upload them to Firebase Storage, after the upload, it gets the downloaded URLs and stores them like:
In order to display those images in the app, I have the following xib file:
Where the middle (big) UIImageView is inside of a UIStackView because it could display more than 1 image.
My problem is that I get the URLs from the database, but for some reason I'm not able to display them in the app. I use the framework SDWebImage for caching and displaying images. So far in the app, it worked fine, but not here. I don't get where I'm wrong.
if let photoURLString = post?.photoURLs
{
for photo in photoURLString
{
let photoURL = URL(string: photo)
print(photoURL!)
postImage.sd_setImage(with: photoURL)
//postImagesStackView.addArrangedSubview(postImage)
}
}
post is the model object. I'm using for in to loop through the array of images from the database. When I print each url, I get the actual URL. But it's simple not displayed. I get this. The images are nowhere to be found/displayed:
Try to add aspect ratio for UIImage. Ex: In xib file, set ratio of UIImage to 1.

Resources