SwiftUI Menu with label an image and a text - swift

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)
}
}

Related

Adding a frame breaks .clipShape() SwiftUI

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 !!
}

SwiftUI Image in App not loading when using Combine

I have been trying to asynchronously load an image in my app using combine. Currently all the other pieces of data are loading fine, but my image seem to be stuck in a progress view. Why? I am not too familiar with how combine works as I have been trying to follow a tutorial and adapting it to fit my needs, which is why I ran into this problem.
This is my code:
Main View:
import SwiftUI
struct ApodView: View {
#StateObject var vm = ApodViewModel()
var body: some View {
ZStack {
// Background Layer
Color.theme.background
.ignoresSafeArea()
// Content Layer
VStack() {
Text(vm.apodData?.title ?? "Placeholder")
.font(.title)
.fontWeight(.bold)
.multilineTextAlignment(.center)
.foregroundColor(Color.theme.accent)
ApodImageView(apodData: vm.apodData ?? ApodModel(date: "", explanation: "", url: "", thumbnailUrl: "", title: ""))
ZStack() {
Color.theme.backgroundTextColor
ScrollView(showsIndicators: false) {
Text(vm.apodData?.explanation ?? "Loading...")
.font(.body)
.fontWeight(.semibold)
.foregroundColor(Color.theme.accent)
.multilineTextAlignment(.center)
.padding()
}
}
.cornerRadius(10)
}
.padding()
}
}
}
ImageView:
import SwiftUI
struct ApodImageView: View {
#StateObject var vm: ApodImageViewModel
init(apodData: ApodModel) {
_vm = StateObject(wrappedValue: ApodImageViewModel(apodData: apodData))
}
var body: some View {
ZStack {
if let image = vm.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
} else if vm.isLoading {
ProgressView()
} else {
Image(systemName: "questionmark")
.foregroundColor(Color.theme.secondaryText)
}
}
.frame(maxWidth: .infinity, maxHeight: 250)
.cornerRadius(10)
}
}
Image ViewModel:
import Foundation
import SwiftUI
import Combine
class ApodImageViewModel: ObservableObject {
#Published var image: UIImage?
#Published var isLoading: Bool = false
private let apodData: ApodModel
private let dataService: ApodImageService
private var cancellables = Set<AnyCancellable>()
init(apodData: ApodModel) {
self.apodData = apodData
self.dataService = ApodImageService(apodData: apodData)
self.addSubscribers()
self.isLoading = true
}
private func addSubscribers() {
dataService.$image
.sink { [weak self] _ in
self?.isLoading = false
} receiveValue: { [weak self] returnedImage in
self?.image = returnedImage
}
.store(in: &cancellables)
}
}
Networking For Image:
import Foundation
import SwiftUI
import Combine
class ApodImageService: ObservableObject {
#Published var image: UIImage?
private var imageSubscription: AnyCancellable?
private let apodData: ApodModel
init(apodData: ApodModel) {
self.apodData = apodData
getApodImage()
}
private func getApodImage() {
guard let url = URL(string: apodData.thumbnailUrl ?? apodData.url) else { return }
imageSubscription = NetworkingManager.download(url: url)
.tryMap({ data -> UIImage? in
return UIImage(data: data)
})
.sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] returnedImage in
self?.image = returnedImage
self?.imageSubscription?.cancel()
})
}
}
General Networking Code:
import Foundation
import Combine
class NetworkingManager {
enum NetworkingError: LocalizedError {
case badURLResponse(url: URL)
case unknown
var errorDescription: String? {
switch self {
case .badURLResponse(url: let url): return "Bad response from URL: \(url)"
case .unknown: return "Unknown Error Returned"
}
}
}
static func download(url: URL) -> AnyPublisher<Data, Error> {
return URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap({ try handleURLResponse(output: $0, url: url) })
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
Where you have:
private func addSubscribers() {
dataService.$image
.sink { [weak self] _ in
self?.isLoading = false
} receiveValue: { [weak self] returnedImage in
self?.image = returnedImage
}
.store(in: &cancellables)
}
You are subscribing to the published value of the image property. That image property stream will never complete. It is an infinite sequence tracking the value of that property over time "forever".
I don't think your receiveCompletion will ever be called so self?.isLoading = false will never happen.

How can I remove a image from the cache with Combine in SwiftUI?

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)
....
}
}

SwiftUI Network Image show different views on loading and error

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).

Async loading from url more than one image in SwiftUI

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)")
}
}