How to use image CDN with SwiftUI? - ios

I want to develop a simple full screen image view in SwiftUI, the image should fit in side the screen, also I want to fetch the image from a CDN so in the request I need to provide the size of the required image.
All the example of SwiffUI I see on the net are some like this.
var body: some View {
Image("my-image")
.frame(width: 375.0, height: 812.0)
}
So I have two question.
How do I fetch image from URL/link
How do I get the size of the image to request, as it will be different for different iOS devices

How do I fetch image from URL/link?
SwiftUI's Image component doesn't provide a native way to load image from an URL, suggest you use one of the open source libraries instead of implementing complex image loading logic, I recommend using SwiftUI version popular open source library SDWebImage, it is called SDWebImageSwiftUI
Here is an example to load an image from image URL
struct ContentView: View {
var body: some View {
WebImage(url:URL(string: "https://<replace with image URL/link>"))
.resizable()
.placeholder {
Rectangle().foregroundColor(.gray)
}
.indicator(.activity)
.scaledToFit()
.frame(width: 375.0, height:812.0)
}
}
How do I get the size of the image to request, as it will be different for different iOS devices?
To get the run-time size of a view, SwiftUI provides a container view called GeometryReader. Whichever component's size you need you have to embed that component(s) inside a GeometryReader container, this container give you access to the run-time size of itself.
Here is complete code to get the run-time size of an image, make a request to image CDN and load the image into full screen view. Here I am using image from imageOpt image CDN you can use any image CDN.
struct ContentView: View {
// Use the URL of imageSet obtained from image CDN
let imageURL = "https://djy8xy980kp8s.cloudfront.net/2e0d6k-p25/q"
var body: some View {
/* Embed the WebImage component inside a GoemetryReader container
* this will give you access to run-time size of the container */
GeometryReader { geo in
WebImage(url:
/* Get the final URL with width & height parameters,
* since you want to fit the image inside full screen
* set crop to false */
imageOptClient.constructURL(imageURL: self.imageURL,
imageSize: CGSize(
// goe.size will give the runtime size of the container
width: geo.size.width,
height: geo.size.height),
crop: false))
.resizable()
.placeholder {
Rectangle().foregroundColor(.gray)
}
.indicator(.activity) // Activity Indicator
.scaledToFit()
.frame(width: geo.size.width, height: geo.size.height,
alignment: .center)
}
}
}
disclaimer: I work for imageOpt

Related

How to make Swift UI adaptive all Screen Size and Text Size in swift ui

can anyone tell how to make Responsive UI in Swift UI, which is compatible on all Device , and using figma Text like 24 then it shows iphone 8 and iphone 11 same view
You'll want to minimize the amount of hardcoded sizes or frames in your app as that will make your app less responsive across different phone models and orientations. Here are some links you can check out to learn responsive UI:
https://www.youtube.com/watch?v=67ZCQ5ihj_I&t=186s
https://www.youtube.com/watch?v=ALzrixd_hd8
Consider trying out geometry reader as well. Here's an article that goes into depth on what it is and how to use it: https://www.hackingwithswift.com/quick-start/swiftui/how-to-provide-relative-sizes-using-geometryreader
For images you can do something like this. You can use GeometryReader class to make your images responsive. Do not use hardcoded size for images. In this snippet I used 50% width of the screen and for height it will also take 50% of the height.
Also Be careful using GeometryReader class. The GeometryProxy returns the current view width and height.
var body: some View {
GeometryReader { reader in
VStack {
Image(contact.imageName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: reader.size.width * 0.5, height: reader.size.width * 0.5)
.clipped()
.cornerRadius(reader.size.width)
.shadow(radius: 10)
}
}
}
A better way to do it without wrapping all your views in a geometry reader is to make an extension to get the screen with and height, which is also callable on all pages without having to do the extension on every page like a geometryreader.
extension View{
func getScreenBounds() -> CGRect{
return UIScreen.main.bounds
}
}
and to call it is the same as geometry reader:
.frame(width: getScreenBounds().width * 0.5, height: getScreenBounds().width * 0.5

Matched Geometry Effect with AsyncImage iOS 15

Consider the following example:
struct ContentView: View {
#State var showSplash: Bool = true
#Namespace var animationNamespace
var body: some View {
ZStack {
if showSplash {
GeometryReader { geometry in
AsyncImage(url: URL(string: "https://picsum.photos/seed/864a5875-6d8b-43d6-8d65-04c5cfb13f3b/1920/1440")) { image in
image.resizable()
.scaledToFill()
.matchedGeometryEffect(id: "SplashImage", in: animationNamespace)
.transition(.move(edge: .bottom))
.frame(width: geometry.size.width)
.transition(.move(edge: .bottom))
.edgesIgnoringSafeArea(.all)
.clipped()
} placeholder: {
Color.gray
}
}
.onTapGesture {
toggleSplashScreen(false)
}
} else {
ScrollView {
GeometryReader { geometry in
AsyncImage(url: URL(string: "https://picsum.photos/seed/864a5875-6d8b-43d6-8d65-04c5cfb13f3b/1920/1440")) { image in
image
image
.resizable()
.scaledToFill()
.matchedGeometryEffect(id: "SplashImage", in: animationNamespace)
.transition(.move(edge: .bottom))
} placeholder: {
Color.gray
}
.frame(width: geometry.size.width, height: 400)
.clipped()
}
.edgesIgnoringSafeArea(.all)
.onTapGesture {
toggleSplashScreen(true)
}
}
}
}
}
}
With a helper method here:
private extension ContentView {
func toggleSplashScreen(_ toggle: Bool) {
withAnimation(.spring(response: 0.85, dampingFraction: 0.95)) {
showSplash = toggle
}
}
}
This produces:
I noticed two things here that I would like to fix
The flashing white effect when transitioning between the two states.
I noticed since we are using AsyncImage, when showSplash changes the AsyncImages only sometimes hits the placeholder block. As a result, the transition becomes really choppy. I tested this with a static image from the assets file and the transition then became smooth. I also tried creating a Caching mechanism on the AsyncImage but still had issues with it hitting placeholder block sometimes.
Would love to hear any ideas :) Thanks!
There are a couple of things that I think you could do to improve this.
First, You are fighting a little bit against the way SwiftUI maintains a view's identity. One of the ways that SwiftUI determines when it can reuse an existing structure as opposed to recreating a structure, is by it's location in the view hierarchy. So when you toggle your structure you go from:
GeometryReader
AsyncImage
to
ScrollView
GeometryReader
AsyncImage
As a result, the system thinks these are two AsyncImage views and so it's rebuilding the view (and reloading the image) every time. I think that's where your white flashes come from since you're seeing your gray placeholder in the middle of your animation. If you could leave the scroll view in place, possibly disabling scrolling when it's not needed (if that's possible) then the OS could maintain the identity of the AsyncImage. (see https://developer.apple.com/videos/play/wwdc2021/10022/)
That leads to the second area of investigation for you. AsyncImage is wonderful in the convenience it gives you in loading content from the network. Unfortunately it doesn't make that communication faster. Your goal should be to have AsyncImage go to the network as few times as possible.
Right now, your resizing strategy focuses on resizing the image. That means that for every transition you're "hitting the network" (read putting your code on the slow, dusty, dirt road path). Instead of resizing the image, you should just load the image once (the slow part) and resize the view that is displaying it. The general idea would be to let AsyncImage load the image, then control how the image is animated by animating the frame of the view.
This is where I get less helpful. I don't know enough about AsyncImage to know if it's capable of implementing that strategy. It seems that it should be... but I don't know that it is. You might have to resort to downloading and storing the image as state separately from the view that presents it.
So my advice is to limit the number of times AsyncImage has to reload the network data. That involves helping SwiftUI maintain the identity of the AsyncImage so it doesn't have to reload each time the view is created. And, try to implement your animations and scaling on the view, not the image, because rescaling the image also requires a network reload.

SwiftUI | customize dragitem appearance within LazyGrid using onDrag [duplicate]

I've been wondering if there is any way to customize the preview image of the view that's being dragged when using onDrag?
As you might see, the preview image is by default a slightly bigger opacity image of the view.
From what I have found, a preview image is generated at the very beginning of the dragging process. But I couldn't find a way to change it.
What I mean by customizing is to have some custom image or a preview image of a custom view. (Both without the default opacity)
Does anyone have an idea?
I have tried to use previewImageHandler and in general, read a lot about the NSItemProvider. But for me, it seems like this is something that is not possible for SwiftUI yet?
With UIKit one could have just customized the UIDragItem - something like that using previewProvider: Here
Here is my demo code:
struct ContentView: View {
var body: some View {
DraggedView()
.onDrag({ NSItemProvider() })
}
private struct DraggedView: View {
var body: some View {
RoundedRectangle(cornerRadius: 20)
.frame(width: 120, height: 160)
.foregroundColor(.green)
}
}
}
I will use this for drag and drop within a LazyVGrid, so custom gestures are unfortunately no option.
One second idea I had would be to have a gesture simultaneously that first changes the item to be dragged to something else and then onDrag starts and returns the NSItemProvider with the preview image which would be then the one I would want. But I couldn't have those two gestures go at the same time, you would have to dismiss one first in order to start the second.
Thank you!
iOS 15 adds an API to do this - you can specify the View to use for the preview. onDrag(_:preview:)
RoundedRectangle(cornerRadius: 20)
.frame(width: 120, height: 160)
.foregroundColor(.green)
.onDrag {
NSItemProvider()
} preview: {
RoundedRectangle(cornerRadius: 18)
.frame(width: 100, height: 140)
.foregroundColor(.green)
}

Remove SwiftUI Form padding

When you use a SwiftUI Form it creates padding on the leading edge, which is fine for Form input but I would like to add additional content to the Form e.g. an image that takes the entire width of the screen, but because the image is in the Form, padding gets applied and the image gets pushed off screen slightly. How can I remove all Form padding?
struct MyForm: View {
var body: some View {
Form {
Image(uiImage: someImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: UIScreen.main.bounds.width)
TextField("Name", text: $name)
// Other fields
}
}
}
I know that the underlying view for Form is a UITableView where I can do things like UITableView.appearance().backgroundColor = .clear to change the Form appearance, but I can't figure out how to remove the leading padding.
I also know I can move the Image view outside the Form and put everything in a stack, but that creates other issues with scrolling that I'd like to avoid.
Here is a solution. Tested with Xcode 11.4 / iOS 13.4
Image(uiImage: someImage)
.resizable()
.aspectRatio(contentMode: .fill)
.listRowInsets(EdgeInsets()) // << this one !!
Note: hardcode to UIScreen.main.bounds.width is not needed

Update SwiftUI View size to match image

I'm trying to make a view which holds an image loaded asynchronously from a network request. Before the image loads, I want to show a placeholder view which has a fixed size. When the image loads, I want to replace this placeholder view with the image, scaled to fit inside the frame of the placeholder view, but then I want the parent view to shrink to match the size of this image. I can't figure out how to do this last part.
Right now, it looks like this:
struct ItemCell: View {
var body: some View {
Group {
CustomImageView(from: imageURL, placeholder: PlaceholderView(), config: { $0.resizable() })
.aspectRatio(contentMode: .fit)
.frame(minWidth: 0, maxWidth: 150, minHeight: 0, maxHeight: 190, alignment: .bottomLeading)
}.background(Color.red) // To show that the view isn't resizing properly
}
}
struct PlaceholderView: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 3, style: .continuous)
.frame(width: 150, height: 190)
.foregroundColor(Color(.secondarySystemBackground))
Image(systemName: "globe")
.resizable()
.scaledToFit()
.frame(width: 50)
.foregroundColor(.secondary)
}
}
}
The CustomImageView is adapted from this article on loading images asynchronously. The ItemCells are placed in a horizontal ScrollView. When I test this, it:
correctly displays the placeholder view before the image is loaded;
resizes the image so it maintains its aspect ratio and fits inside the 150x190 frame, but has a weird animation where some of the images shrink and then expand back; also, some of the images seem to shrink too much;
does not resize the parent view to match the size of the image properly, but instead retains the full original height and some (?) extra width on some cells.
These two problems are shown in the gif below, with blue images and a red background. Notice the extra height on the first and third cells, and the extra width on the second. Also, note that the first image ends up smaller than when it first loads, even though it fit inside the original 150x190 frame at first.
How can I fix these problems?
Figured out how to do it. There were several problems with my original code. First, the ItemCells used in the ScrollView should be modified with the .fixedSize() view modifier, like so:
ScrollView(...) {
HStack(...) {
ForEach(...) { ...
ItemCell()
.fixedSize()
}
}
}
Then, changing the frame of the CustomImageCell to be use idealHeight instead of maxHeight and making the Group a VStack with a Spacer() to push everything to the bottom, as #Paulw11 had suggested in comments:
struct ItemCell: View {
var body: some View {
VStack {
Spacer()
CustomImageView(from: imageURL, placeholder: PlaceholderView(), config: { $0.resizable() })
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 150, idealHeight: 190)
}
}
}
These changes fix both the image resizing animation issue and the extra space issue.

Resources