SDWebImageSwiftUI - How to show default image before user selection - swift

I have a profile picture that I am attempting to show a default image until a user selects a picture from a picker (say that three times fast). In WebImage, I point to a view Model, which connects to my database to grab the user image. If no image exists I currently just have it on an empty string.
For reference, this is the line that serves up the image
WebImage(url: URL(string: vm.userModel?.profilePictureURL ?? "")
I tried adding a bool that updated on the onChange function to true and tried wrapping the webImage in an IF statement, but the view didn't update correctly.
import SwiftUI
import Firebase
import FirebaseFirestore
import SDWebImageSwiftUI
struct ProfilePicture: View {
#ObservedObject var vm = DashboardLogic()
#State private var showingImagePicker = false
#State private var inputImage: UIImage?
var body: some View {
ZStack (alignment: .topTrailing){
VStack{
if let inputImage = inputImage {
Image(uiImage: inputImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:150, height: 150)
.clipShape(Circle())
} else{
WebImage(url: URL(string: vm.userModel?.profilePictureURL ?? ""))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:150, height: 150)
.clipShape(Circle())
}
Button(action: {
showingImagePicker = true
}){
Image(systemName:("plus.circle.fill"))
.resizable()
.aspectRatio(contentMode: .fill)
.foregroundColor(Color("ButtonTwo"))
.frame(width:30, height:25)
.contentShape(Rectangle())
}
//PRESENT PICKER
}
.sheet(isPresented: $showingImagePicker){
EditorImagePicker(image: $inputImage)
}
}
//SAVE IMAGE TO DATABASE (FIREBASE)
.onChange(of: inputImage, perform: { _ in
persistImageToStorage() //call to save function
})
}
}
}

Related

Image disappearing after adding \(var) in Image

I am a beginner to Swift, SwiftUI, and using Xcode. I have been doing a couple of online tutorials and came across a problem when doing this bonus challenge.
import SwiftUI
struct ContentView: View {
#State var userCredits = 1000
#State var slot1 = 1
#State var slot2 = 1
#State var slot3 = 1
var body: some View {
VStack {
Text("SwiftUI Slot!")
.font(.system(size: 38))
.fontWeight(.medium)
Spacer()
Text("Credits: \(userCredits)")
Spacer()
HStack{
Image("apple\(slot1)")
.resizable()
.aspectRatio(contentMode: .fit)
Image("cherry\(slot2)")
.resizable()
.aspectRatio(contentMode: .fit)
Image("star\(slot3)")
.resizable()
.aspectRatio(contentMode: .fit)
}
Spacer()
Button("Spin") {
slot1 = Int.random(in: 1...3)
slot2 = Int.random(in: 1...3)
slot3 = Int.random(in: 1...3)
if slot1 == slot2 && slot2 == slot3{
userCredits += 15
}
else {
userCredits -= 5
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The images of the apple, cherry, and star disappear when I put the (slot#) after it
I tried taking out the (slot#) but that will simply display the picture of the images and not change them nor make the game work as intended
You could do this by declaring an array of image names:
let imageNames = ["apple", "cherry", "star"]
then use something like:
HStack{
Image(imageNames[slot1])
.resizable()
.aspectRatio(contentMode: .fit)
Image(imageNames[slot2])
.resizable()
.aspectRatio(contentMode: .fit)
Image(imageNames[slot3])
.resizable()
.aspectRatio(contentMode: .fit)
}
Also, don't forget that Swift arrays are zero based, so your spin function should be:
slot1 = Int.random(in: 0..<3)
slot2 = Int.random(in: 0..<3)
slot3 = Int.random(in: 0..<3)
What you need is to add your images into an array and then use the values for your slot# properties as indices for getting an image from the array
So add an array property first to your view
let images = ["apple", "cherry", "star"]
Then change the call to Image(...) to use this array
Image(images[slot1 - 1])
and so on for the other two

Swift - recognize a link and make it clickable

I have an app that loads blog posts from a Wordpress site.
Now loading the text, images and video's work fine.
In the blog posts from Wordpress are links. Like for example: https://eenvandaag.avrotros.nl/item/ondanks-droogte-op-de-veluwe-onttrekt-kartonfabriek-parenco-er-jaarlijks-miljarden-liters-grondwater/
When the app loads the post, the link is displayed in plain text.
Is there a way to recognise the links and make these "links" clickable and guide the user to safari?
I've been searching the internet for hours but couldn't find a fitting solution.
Every post has a different link. So the goal is that the link is recognised and made clickable.
I found some examples, for example:
var body: some View {
VStack {
Text(attributedString)
Text("Check out [this link](https://stack.com)")
}
}
var attributedString: AttributedString {
var attributedText = AttributedString("Visit website")
attributedText.link = URL(string: "https://stack.com")
return attributedText
}
But then you have to set the link.
So that doesn't work for me since the link is different for every post.
Can someone point me in the right direction?
This is the code for my PostContentView
import AVKit
import CachedAsyncImage
import SwiftUI
import WPArticleView
struct PostContentView: View {
let post: Post
var body: some View {
VStack(alignment: .leading, spacing: 32) {
if let url = post.embedded.medias?.first?.url {
CachedAsyncImage(url: url, urlCache: .imageCache) { phase in
switch phase {
case.empty:
ProgressView()
.progressViewStyle(.circular)
.frame(maxWidth: .infinity)
case.success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.cornerRadius(8)
.frame(height: 200)
.padding(.bottom, 16)
.padding(.top, -10)
case.failure:
ZStack{
Image(systemName: "wifi.slash")
.foregroundColor(Color(UIColor.white))
RoundedRectangle(cornerRadius: 6)
.foregroundColor(Color(UIColor.systemGray5))
.frame(height: 200)
.frame(maxWidth: .infinity)
}
#unknown default:
EmptyView()
}
}
}
VStack(alignment: .leading, spacing: 16) {
Text(post.title.text)
.font(.title2).bold()
.padding(.horizontal)
.foregroundColor(.white)
Text(post.date.formattedDateString)
.font(.footnote)
.padding(.horizontal)
.foregroundColor(.white)
WPArticleView(htmlBody: post.content.rendered) { blocks in
ForEach(blocks.indices, id: \.self) { id in
blocks[id]
.padding(.bottom, 6)
.padding(.horizontal)
.foregroundColor(.white)
.textSelection(.enabled)
}
} text: { attributedText in
Text(attributedText)
} image: { imageURL in
CachedAsyncImage(url: imageURL, urlCache: .imageCache) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.cornerRadius(8)
} placeholder: {
//EmptyView()
ProgressView()
.frame(maxWidth: .infinity)
}
} video: { videoURL in
VideoPlayer(player: AVPlayer(url: videoURL))
.frame(minHeight: 250)
//.frame(minHeight: 400)
.cornerRadius(8)
}
}
}
.padding(.horizontal, 20)
}
}
And my Post screen:
import Foundation
struct Post: Decodable, Identifiable {
let id: Int
let date: Date
let title: Title
let content: Content
let embedded: Embedded
enum CodingKeys: String, CodingKey {
case date = "date"
case title = "title"
case content = "content"
case embedded = "_embedded"
case id = "id"
}
}
EDIT:
Here is an example from a post:

Swiftui failed to produce diagnostic for expression; please submit a bug report

Before I select an image from the gallery, I want to have an initial image from SF-Symbole ("photo.fill). But unfortunately it shows me an error.
Failed to produce diagnostic for expression; please submit a bug
report (https://swift.org/contributing/#reporting-bugs) and include
the project
The code:
import SwiftUI
struct ContentView: View {
#State private var showImagePicker : Bool = false
#State private var image : Image? = nil
var body: some View {
NavigationView{
VStack {
Spacer()
if image? {
Image(uiImage: image?)
.resizable()
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity)
} else {
Image(systemName: "photo.fill")
.resizable()
.scaledToFit()
.opacity(0.6)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(.horizontal)
}
Spacer()
Button("Öffne Galerie"){
self.showImagePicker = true
}.padding()
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(10)
}
.sheet(isPresented: self.$showImagePicker) {
PhotoCaptureView(showImagePicker: self.$showImagePicker, image: self.$image)
}
.navigationBarTitle(Text("Foto bearbeiten"))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
As I mentioned in my comment you are unwrapping the optional image incorrectly, you need to use an if-let so that you have a non-nil value for your image.
You are also passing an Image where you actually require a UIImage. This is not something that you can do. You need to make sure that you understand that types that you using and what they represent.
Unfortunately you did not include the code for your PhotoCaptureView so it is not possible to see what you actually require. This is why you are asked to provide a minimum reproducible example when you post a question on SO.
However, here is how you can handle it if it requires an Image or a UIImage, there are similarities to both. Look at the comments in the code for the changes.
PhotoCaptureView uses Image
If you are creating an Image in the PhotoCaptureView then there are two changes that you need to make.
You need to unwrap your Image before you use it
As you have a SwiftUI Image you can just use it directly.
struct ContentView: View {
#State private var showImagePicker : Bool = false
#State private var image : Image? = nil
var body: some View {
NavigationView{
VStack {
Spacer()
if let image = image { // here we unwrap the Image
image // as we have a SwiftUI Image we can use it directly.
.resizable()
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity)
} else {
Image(systemName: "photo.fill")
.resizable()
.scaledToFit()
.opacity(0.6)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(.horizontal)
}
Spacer()
Button("Öffne Galerie"){
self.showImagePicker = true
}.padding()
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(10)
}
.sheet(isPresented: self.$showImagePicker) {
PhotoCaptureView(showImagePicker: self.$showImagePicker, image: self.$image)
}
.navigationBarTitle(Text("Foto bearbeiten"))
}
}
}
PhotoCaptureView uses UIImage
However, if your PhotoCaptureView requires a UIImage then you need to make three changes to your code.
Firstly we would need to change your #State variable from being Image into a UIImage.
We can then unwrap the image the same way as above
We then pass the unwrapped UIImage into the initializer for Image(uiImage:)
struct ContentView: View {
#State private var showImagePicker : Bool = false
#State private var image : UIImage? = nil // Here we use UIImage
var body: some View {
NavigationView{
VStack {
Spacer()
if let image = image { // We unwrap the UIImage so that we can use it
Image(uiImage: image) // Here we convert the UIImage into a SwiftUI Image
.resizable()
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity)
} else {
Image(systemName: "photo.fill")
.resizable()
.scaledToFit()
.opacity(0.6)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(.horizontal)
}
Spacer()
Button("Öffne Galerie"){
self.showImagePicker = true
}.padding()
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(10)
}
.sheet(isPresented: self.$showImagePicker) {
PhotoCaptureView(showImagePicker: self.$showImagePicker, image: self.$image)
}
.navigationBarTitle(Text("Foto bearbeiten"))
}
}
}
I would suggest that you read up on Optionals in the Swift documentation, as you seem to have some misconceptions about them.

How to get AsyncImage url path from url with SwiftUI

My SwiftUI app displays images from an external url properly using
LazyVGrid(columns: columns, alignment: .center, spacing: 10) {
ForEach(0..<14) { i in
AsyncImage(url: url) { image in
VStack {
image
.resizable()
.scaledToFill()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(10)
.onTapGesture {
selectedItem = ImageSelection(name: url!.path)
print(selectedItem?.name as Any)
}
}
}
.sheet(item: $selectedItem) { item in
Image(item.name)
}
But the sheet that comes up from the .onTapGesture is blank. How can I properly get the url path so the image displays on the new sheet? Thanks!
EDIT
Ultimately this view is displaying images from https://picsum.photos. I'm trying to determine the actual URL of the displayed images.
as #Asperi mentioned, you could use another AsyncImage to again download the image. Try the following code, which
fixes some of the inconsistencies in your code and also loops over
the url id (as per your latest thought) to download each different ones:
struct ImageSelection: Identifiable {
let id = UUID()
var url: URL? // <-- note
}
struct ContentView: View {
let columns:[GridItem] = Array(repeating: .init(.flexible(), spacing: 5), count: 3)
#State var selectedItem: ImageSelection?
var body: some View {
ScrollView {
LazyVGrid(columns: columns, alignment: .center, spacing: 10) {
ForEach(0..<14) { i in
let url = URL(string: "https://picsum.photos/id/\(i)/200")
AsyncImage(url: url) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(10)
.onTapGesture {
selectedItem = ImageSelection(url: url)
}
}
else {
Image(systemName: "smiley")
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
}
}
}
}
.sheet(item: $selectedItem) { item in
AsyncImage(url: item.url)
}
}
}
}

Asynchronous Image Loading Bug

I am hitting a strange bug when it comes to Asynchronous Image Loading, where when I enter a view, the shows up like it is suppose to do, then for some reason the image drops and all I see is the "Loading..." placeholder. I used this tutorial when building my loader and the following script is my Article View. I have a Global Functions file, which includes reference to Combine and Foundation for my various functions through out the app. I am just not fully understanding why the image is showing for a brief moment, then calls the placeholder. Thanks!
import SwiftUI
struct ArticleView: View {
#Environment(\.imageCache) var cache: ImageCache
var articleID: Int
#ObservedObject private var data = Result2()
let defaults = UserDefaults.standard
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
init(articleID: Int){
self.articleID = articleID
self.data.results.append(Story.init(id: 0, title: "test", image: "", story: "", published: "", author: ""))
self.loadArticle(CDNLink: "http://\(self.defaults.object(forKey: "domain") as! String)/cdn?funct=fetchArticle&articleID=\(self.articleID)")
}
var backBtn: some View {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label:{
Image(systemName: "lessthan.circle")
.resizable()
.frame(width: 30, height: 30)
.foregroundColor(Color.primaryRed)
.padding(.top, 6)
.padding(.leading, 10)
})
}
var body: some View {
VStack(alignment: .leading){
ScrollView {
ForEach(data.results, id: \.id) { result in
Group{
AsyncImage(
url: URL(string: result.image)!,
placeholder: Text("Loading..."), configuration: { $0.resizable() })
.frame(height: 250)
.aspectRatio(contentMode: .fill)
VStack(alignment: .leading) {
Text("\(result.title)")
.font(.system(size: 18))
.fontWeight(.heavy)
.foregroundColor(.primary)
.multilineTextAlignment(.leading)
HStack {
Text("\(result.author)")
.font(.system(size: 12))
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
Text("|")
.font(.system(size: 12))
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
Text("\(result.published)")
.font(.system(size: 12))
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
}.frame(width: UIScreen.main.bounds.size.width - 40, alignment: .leading)
}.frame(width: UIScreen.main.bounds.size.width - 40)
}
Text("\(result.story)")
}
}
}
.frame(maxWidth: .infinity)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backBtn)
}
func loadArticle(CDNLink: String) {
guard let url = URL(string: CDNLink) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if error != nil {
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
return
} else {
do {
let decodedResponse = try JSONDecoder().decode(Response2.self, from: data!)
print(decodedResponse)
DispatchQueue.main.async {
self.data.results.append(contentsOf: decodedResponse.results)
self.data.results.remove(at: 0)
}
} catch let err {
print("Error parsing: \(err)")
}
}
}.resume()
}
}
struct Response2: Codable {
var results: [Story]
}
struct Story: Codable, Identifiable {
var id: Int
var title: String
var image: String
var story: String
var published: String
var author: String
}
struct ArticleView_Previews: PreviewProvider {
static var previews: some View {
ArticleView(articleID: 0)
}
}
Edit: I have made the changes suggested and I am still seeing the error, you can see the error live by checking out this link.
Hi most likely it is due to the fact that you are using #ObservedObject. This object is discarded and initialized every time your view state changes and is triggering a view re-render. Try using #StateObject instead of #ObservedObject. The #StateObject will be initialized only once in the view.
A #StateObject can be also used with an ObservedObject model. It is kind of a combination of #ObservedObject and #State.
Okay so I created a new solution to patch this issue up and I am very glad that this little work around is working. So, I started looking at what I could do server side to optimize content being pulled into the app. What I did was use PHP to encode the image with base64, then the app pulls it in and decodes the base64 data and the image populates with lightning speed!
Server Side code:
$article['image'] = base64_encode(file_get_contents(PATH . "/cache/content/topstory/" . $row['app_article']));
Client Side code:
let dataDecoded:NSData = NSData(base64Encoded: result.image, options: NSData.Base64DecodingOptions(rawValue: 0))!
let decodedimage:UIImage = UIImage(data: dataDecoded as Data)!
Image(uiImage: decodedimage)
.frame(height: 250)
.aspectRatio(contentMode: .fill)