I'm trying to write a code that loads more than one image (between 5 and 10) from url. I use Theaudiodb api (loaded images are photos of artists). The problem is that only last image is displayed. In ContentView I have ForEach that loads between 5 and 10 ArtistImage(s) struct. I know that all images data are loaded (in ImageFetcher var imageData will set I put print(newValue) and it shows data loaded (for example: 148273 bytes ,etc). Just cannot understand why just the last image is displayed.
struct ArtistImage: View {
var imageURL: URL?
#ObservedObject private var imageFetcher: ImageFetcher
init(imageURL: String) {
self.imageURL = URL(string: imageURL)
imageFetcher = ImageFetcher(imageURL: self.imageURL)
}
var body: some View {
if let uiImage = UIImage(data: imageFetcher.imageData) {
return AnyView(Image(uiImage: UIImage(data: self.imageFetcher.imageData)!)
.renderingMode(.original)
.resizable()
.cornerRadius(10))
} else {
return AnyView(Text("Loading...")
.onAppear(perform: { self.imageFetcher.getImage() }))
}
}
and the ImageFetcher class below:
class ImageFetcher: ObservableObject {
public let objectWillChange = PassthroughSubject<Data,Never>()
public private(set) var imageData = Data() {
willSet {
print(newValue) // it tells me all images are loaded
objectWillChange.send(newValue)
}
}
var imageURL: URL?
public init(imageURL: URL?) {
self.imageURL = imageURL
}
func getImage() {
guard let url = imageURL else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else {
return
}
DispatchQueue.main.async {
self.imageData = data
}
}.resume()
}
Content View:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
init() {
networkManager.createArtistsDatabase()
}
var body: some View {
ScrollView(.vertical) {
GeometryReader { geo in
Button( action: {} ) {
ZStack(alignment: .bottom) {
Image("cp")
.renderingMode(.original)
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width, height: geo.size.height)
.clipped()
VStack(alignment: .center) {
Text("Featured artist").foregroundColor(.yellow).font(.custom("AvenirNext-Regular",size: 16)).padding(.bottom, 8)
Text(("Coldplay").uppercased()).foregroundColor(.white).font(.custom("AvenirNext-Bold", size:24))
}.frame(width: geo.size.width, height: 80).background(Color.black.opacity(0.5))
}
}
}.frame(height: 400.0)
VStack(alignment:.leading){
Text("Discover new artists").font(.custom("AvenirNext-Regular", size: 16)).padding(.top,10)
ScrollView(.horizontal) {
HStack {
ForEach(self.networkManager.artistsDB, id:\.self) { data in
Button(action:{} ) {
VStack {
ArtistImage(imageURL: data.artistImage)
.frame(width: 150, height: 150)
.cornerRadius(10)
Text(data.artistName).foregroundColor((.secondary)).font(.custom("AvenirNext-Regular", size: 14))
}
}
}
}.frame(height: 180)
}
}.frame(minWidth: 0, maxWidth:.infinity,maxHeight: .infinity, alignment: .leading).padding(.leading, 10)
}.edgesIgnoringSafeArea(.top)
}
}
Network Manager class:
class NetworkManager: ObservableObject {
#Published var artistsDB = [Artist]()
func createArtistsDatabase() {
let artistsNames: [String] = ["Madonna", "Metallica","Coldplay","Toto","Kraftwerk","Depeche%20Mode"]
for ar in artistsNames {
findArtistBy(name: ar)
}
}
func findArtistBy(id: String = "", name: String = "") {
var url: URL?
if id != "" {
guard let urlID = URL.getArtistByID(id: id) else { return }
url = urlID
} else {
guard let urlName = URL.getArtistByName(name: name) else { return }
url = urlName
}
let urlRequest = URLRequest(url: url!)
let task = URLSession.shared.dataTask(with: urlRequest) { data,response,error in
if error != nil || data == nil {
print("Client error")
return
}
guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
print("Server error")
return
}
guard let mime = response.mimeType, mime == "application/json" else {
print("Wrong mime type")
return
}
guard let dataToDecode = data else { return }
do {
let decoder = JSONDecoder()
let dataJSONed = try decoder.decode(DBArtist.self, from: dataToDecode)
DispatchQueue.main.async {
print(dataJSONed.artist[0])
self.artistsDB.append(dataJSONed.artist[0])
}
} catch {
print("Error while decoding!")
}
}
task.resume()
}
URL extension:
extension URL {
static func getArtistByID(id: String) -> URL? {
return URL(string: "https://theaudiodb.com/api/v1/json/1/artist.php?i=\((id))")
}
static func getArtistByName(name: String) ->URL? {
print("https://www.theaudiodb.com/api/v1/json/1/search.php?s=\(name)")
return URL(string: "https://www.theaudiodb.com/api/v1/json/1/search.php?s=\(name)")
}
}
Related
So I'm loading the image in a custom struct called 'RemoteImage' that takes in a URL and loads it into a SwiftUI Image(). For some reason, when I have .clipShape() on the RemoteImage(), it clips the shape to a circle (although it's the wrong width/height, but when I set the .frame() to the desired width/height, the image is displayed with the correct width/height but is no longer a circle. Not sure how to fix:
RemoteImage(url: image)
.clipShape(Circle())
.frame(width: 82, height: 82)
struct RemoteImage: View {
private enum LoadState {
case loading, success, failure
}
private class Loader: ObservableObject {
var data = Data()
var state = LoadState.loading
init(url: String) {
guard let parsedURL = URL(string: url) else {
fatalError("Invalid URL: \(url)")
}
URLSession.shared.dataTask(with: parsedURL) { data, response, error in
if let data = data, data.count > 0 {
self.data = data
self.state = .success
} else {
self.state = .failure
}
DispatchQueue.main.async {
self.objectWillChange.send()
}
}.resume()
}
}
#StateObject private var loader: Loader
var loading: Image
var failure: Image
var body: some View {
selectImage()
.resizable()
}
init(url: String, loading: Image = Image(""), failure: Image = Image(systemName: "multiply.circle")) {
_loader = StateObject(wrappedValue: Loader(url: url))
self.loading = loading
self.failure = failure
}
private func selectImage() -> Image {
switch loader.state {
case .loading:
return loading
case .failure:
return failure
default:
if let image = UIImage(data: loader.data) {
return Image(uiImage: image)
} else {
return failure
}
}
}
}
Works fine, Xcode 13.3 / iOS 15.4, however clipShape is better to apply after frame.
Let me suppose that you just wanted aspect ratio
var body: some View {
selectImage()
.resizable()
.aspectRatio(contentMode: .fit) // << here !!
}
I'm building a SwiftUI app with CRUD functionality with a mysql db. The CRUD operations work fine and after the create, update, or delete function the read function is called to get the updated data from the db. When I run the app the read function seems to be called before the create, update of delete function if I look at the console output so I only see the mutation after running the app again. Any idea how to fix this?
This is my create code
struct NewPostView: View {
#EnvironmentObject var viewModel: ViewModel
#Binding var isPresented: Bool
#Binding var title: String
#Binding var post: String
#State var isAlert = false
var body: some View {
NavigationView {
ZStack {
Color.gray.opacity(0.1).edgesIgnoringSafeArea(.all)
VStack(alignment: .leading) {
Text("Create new post")
.font(Font.system(size: 16, weight: .bold))
TextField("Title", text: $title)
.padding()
.background(Color.white)
.cornerRadius(6)
.padding(.bottom)
TextField("Write something...", text: $post)
.padding()
.background(Color.white)
.cornerRadius(6)
.padding(.bottom)
Spacer()
}.padding()
.alert(isPresented: $isAlert, content: {
let title = Text("No data")
let message = Text("Please fill your title and post")
return Alert(title: title, message: message)
})
}
.navigationBarTitle("New post", displayMode: .inline)
.navigationBarItems(leading: leading, trailing: trailing)
}
}
var leading: some View {
Button(action: {
isPresented.toggle()
}, label: {
Text("Cancel")
})
}
var trailing: some View {
Button(action: {
if title != "" && post != "" {
let parameters: [String: Any] = ["title": title, "post": post]
print(parameters)
viewModel.createPost(parameters: parameters)
viewModel.fetchPosts()
isPresented.toggle()
} else {
isAlert.toggle()
}
}, label: {
Text("Post")
})
}
}
ViewModel
class ViewModel: ObservableObject {
#Published var items = [PostModel]()
let prefixURL = "https://asreconnect.nl/CRM/php/app"
init() {
fetchPosts()
}
//MARK: - retrieve data
func fetchPosts() {
guard let url = URL(string: "\(prefixURL)/posts.php") else {
print("Not found url")
return
}
URLSession.shared.dataTask(with: url) { (data, res, error) in
if error != nil {
print(data!)
print("error", error?.localizedDescription ?? "")
return
}
do {
if let data = data {
print(data)
let result = try JSONDecoder().decode(DataModel.self, from: data)
DispatchQueue.main.async {
self.items = result.data
print(result)
}
} else {
print("No data")
}
} catch let JsonError {
print("fetch json error:", JsonError.localizedDescription)
print(String(describing: JsonError))
}
}.resume()
}
//MARK: - create data
func createPost(parameters: [String: Any]) {
guard let url = URL(string: "\(prefixURL)/create.php") else {
print("Not found url")
return
}
let data = try! JSONSerialization.data(withJSONObject: parameters)
print(data)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = data
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { (data, res, error) in
if error != nil {
print("error", error?.localizedDescription ?? "")
return
}
do {
if let data = data {
let result = try JSONDecoder().decode(DataModel.self, from: data)
DispatchQueue.main.async {
print(result)
}
} else {
print("No data")
}
} catch let JsonError {
print("fetch json error:", JsonError.localizedDescription)
}
}.resume()
}
}
As you can see in the image I would like to put the photo inside that drop down menu where it says Paul.
I tried to insert it but I get a strange effect, it happens that the image takes up all the space, you can't see the text anymore.
Can you give me a hand?
import Foundation
import SwiftUI
struct RemoteImage: View {
private enum LoadState {
case loading, success, failure
}
private class Loader: ObservableObject {
var data = Data()
var state = LoadState.loading
init(url: String) {
guard let parsedURL = URL(string: url) else {
fatalError("Invalid URL: \(url)")
}
URLSession.shared.dataTask(with: parsedURL) { data, response, error in
if let data = data, data.count > 0 {
self.data = data
self.state = .success
} else {
self.state = .failure
}
DispatchQueue.main.async {
self.objectWillChange.send()
}
}.resume()
}
}
#StateObject private var loader: Loader
var loading: Image
var failure: Image
var body: some View {
selectImage()
.resizable()
}
init(url: String, loading: Image = Image(systemName: "photo"), failure: Image = Image(systemName: "multiply.circle")) {
_loader = StateObject(wrappedValue: Loader(url: url))
self.loading = loading
self.failure = failure
}
private func selectImage() -> Image {
switch loader.state {
case .loading:
return loading
case .failure:
return failure
default:
if let image = NSImage(data: loader.data) {
return Image(nsImage: image)
} else {
return failure
}
}
}
}
struct Test: View {
var body: some View {
VStack(alignment: .trailing){
Menu {
Button(action: {
NSApplication.shared.terminate(self)
}) {
Text("Esci")
}
} label: {
RemoteImage(url: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Leonardo_Dicaprio_Cannes_2019_2.jpg/440px-Leonardo_Dicaprio_Cannes_2019_2.jpg")
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.cornerRadius(10)
Text("Paul")
}
.menuStyle(BorderlessButtonMenuStyle())
.frame(width: 80, height: 16)
.padding(3)
.cornerRadius(4)
.help("Impostazioni")
}
.frame(width: 360, height: 360)
}
}
When I delete the data from array, the operation is successful, but only the picture does not change. The image still remains in the cache. However, when I close and open the application, the application works fine.
how can i update the cache?
Image Loader
import Combine
class ImageLoader: ObservableObject {
#Published var image: UIImage?
private let url: String
private var cancellable: AnyCancellable?
private var cache: ImageCache?
init(url: String, cache: ImageCache? = nil) {
self.url = url
self.cache = cache
}
deinit {
cancel()
}
func load() {
guard let cacheURL = URL(string: url) else { return }
if let image = cache?[cacheURL] {
self.image = image
return
}
guard let url = URL(string: url) else { return }
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.handleEvents(receiveOutput: { [weak self] in self?.cache($0) })
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
private func cache(_ image: UIImage?) {
guard let cacheURL = URL(string: url) else { return }
image.map { cache?[cacheURL] = $0 }
}
func cancel() {
cancellable?.cancel()
}
}
Async Image
import Combine
struct AsyncImage<Placeholder: View>: View {
#StateObject private var loader: ImageLoader
private let placeholder: Placeholder
init(url: String, #ViewBuilder placeholder: () -> Placeholder) {
self.placeholder = placeholder()
_loader = StateObject(wrappedValue: ImageLoader(url: url, cache: Environment(\.imageCache).wrappedValue))
}
var body: some View {
content
.onAppear(perform: loader.load)
}
private var content: some View {
Group {
if loader.image != nil {
Image(uiImage: loader.image!)
.resizable()
} else {
placeholder
}
}
}
}
Image Cache
protocol ImageCache {
subscript(_ url: URL) -> UIImage? { get set }
}
Temporary Image Cache
struct TemporaryImageCache: ImageCache {
private let cache = NSCache<NSURL, UIImage>()
subscript(_ key: URL) -> UIImage? {
get { cache.object(forKey: key as NSURL) }
set { newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) }
}
}
Image Cache Key
struct ImageCacheKey: EnvironmentKey {
static let defaultValue: ImageCache = TemporaryImageCache()
}
extension EnvironmentValues {
var imageCache: ImageCache {
get { self[ImageCacheKey.self] }
set { self[ImageCacheKey.self] = newValue }
}
}
Async Image Using
VStack {
ZStack(alignment: .bottomTrailing) {
AsyncImage(url: "image url") {
Text("Loading")
}
.aspectRatio(contentMode: .fill)
.frame(width: 120, height: 180, alignment: .center)
.cornerRadius(15)
....
}
}
I would like to make sure that when the image is being downloaded the ProgressView is visible and if the url being passed is invalid (empty), a placeholder is put.
How can I do this?
Code:
import Foundation
import SwiftUI
// Download image from URL
struct NetworkImage: View {
public let url: URL?
var body: some View {
Group {
if let url = url, let imageData = try? Data(contentsOf: url),
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
}
else {
ProgressView()
}
}
}
}
struct NetworkImage_Previews: PreviewProvider {
static var previews: some View {
let url = [
"",
"http://google.com",
"https://yt3.ggpht.com/a/AATXAJxAbUTyYnKsycQjZzCdL_gWVbJYVy4mVaVGQ8kRMQ=s176-c-k-c0x00ffffff-no-rj"
]
NetworkImage(url: URL(string: url[0])!)
}
}
You can create a ViewModel to handle the downloading logic:
extension NetworkImage {
class ViewModel: ObservableObject {
#Published var imageData: Data?
#Published var isLoading = false
private var cancellables = Set<AnyCancellable>()
func loadImage(from url: URL?) {
isLoading = true
guard let url = url else {
isLoading = false
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.imageData = $0
self?.isLoading = false
}
.store(in: &cancellables)
}
}
}
And modify your NetworkImage to display a placeholder image as well:
struct NetworkImage: View {
#StateObject private var viewModel = ViewModel()
let url: URL?
var body: some View {
Group {
if let data = viewModel.imageData, let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "photo")
}
}
.onAppear {
viewModel.loadImage(from: url)
}
}
}
Then you can use it like:
NetworkImage(url: URL(string: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png"))
(Note that the url parameter is not force unwrapped).