Search code examples
iosswiftswiftuiswiftui-asyncimage

Moving to details view with Image in SwiftUI


I am new to swift. I am trying to achieve the requirement is when the spacific view record is selected I am want to move into details view with selected record including image. But the problems is the text property is display as expected not image failed.

so far I have tried Image(people.avatar ?? "") but actually it not working. even I can not see it into preview as well.

Here is the content view code ..

import SwiftUI

struct PeopleListView: View {

    @StateObject var viewModel: PeopleListViewModel

    var body: some View {
        NavigationStack {
            VStack {
                if viewModel.customError != nil && !viewModel.refreshing {
                    alertView()
                } else {
                    if viewModel.refreshing {
                        progressView()
                    }
                    if viewModel.peopleLists.count > 0 && !viewModel.refreshing {

                        List(viewModel.peopleLists, id: \.self) { people in
                            NavigationLink(destination: PeopleDetailsView(people: people)) {

                                PeopleCellView(people: people)
                            }
                        }
                    }
                }
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    getToolBarView()
                }
            }
            .navigationTitle(Text("Employees List"))
        }.task{
            await getDataFromAPI()
        }
        .refreshable {
            await getDataFromAPI()
        }
    }

    func getDataFromAPI() async {
        await viewModel.getPeopleList(urlStr: NetworkURL.peopleUrl)
    }

    @ViewBuilder
    func getToolBarView() -> some View {
        Button {
            Task{
                await getDataFromAPI()
            }
        } label: {
            HStack {
                Image(systemName: "arrow.clockwise")
                    .padding(.all, 10.0)
            }.fixedSize()
        }
        .cornerRadius(5.0)
    }

    @ViewBuilder
    func progressView() -> some View {
        VStack{
            RoundedRectangle(cornerRadius: 15)
                .fill(.white)
                .frame(height: 180)
                .overlay{
                    VStack{
                        ProgressView().padding(50)
                        Text("Please Wait Message").font(.headline)
                    }
                }
        }
    }

    @ViewBuilder
    func alertView() -> some View {
        Text("").alert(isPresented: $viewModel.isErrorOccured) {
            Alert(title: Text("General_Error"), message: Text(viewModel.customError?.localizedDescription ?? ""),dismissButton: .default(Text("Okay")))
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        PeopleListView(viewModel: PeopleListViewModel(repository: PeopleRepositoryImplementation(networkManager: NetworkManager())))
    }
}

Here is code for cashed ..

import SwiftUI

struct CacheAsyncImage<Content>: View where Content: View {
    private let url: URL
    private let scale: CGFloat
    private let transaction: Transaction
    private let content: (AsyncImagePhase) -> Content

    init(url: URL,
         scale: CGFloat = 1.0,
         transaction: Transaction = Transaction(),
         @ViewBuilder content: @escaping (AsyncImagePhase) -> Content) {
        self.url = url
        self.scale = scale
        self.transaction = transaction
        self.content = content
    }
    var body: some View {
        if let cached = ImageCache[url] {
            content(.success(cached))
        } else {
            AsyncImage(url: url,
                       scale: scale,
                       transaction: transaction) { phase in
                cacheAndRender(phase: phase)
            }
        }
    }
    func cacheAndRender(phase: AsyncImagePhase) -> some View {
        if case .success(let image) = phase {
            ImageCache[url] = image
        }
        return content(phase)
    }
}

fileprivate class ImageCache {
    static private var cache: [URL: Image] = [:]
    static subscript(url: URL) -> Image? {
        get {
            ImageCache.cache[url]
        }
        set {
            ImageCache.cache[url] = newValue
        }
    }
}

Here is code for async image cashed ..

import SwiftUI

struct PeopleAsyncImageView: View {
    let url: URL
    private let imageWidth = 150.0
    private let cellHeight = 150.0

    var body: some View {
        CacheAsyncImage(
            url: url) { phase in
                switch phase {
                case .success(let image):
                    VStack {
                        image
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: imageWidth)
                            .padding(.trailing, 10)
                        Spacer()
                    }
                case .failure:
                    Image("placeholder-image")

                case .empty:
                    HStack(alignment: .center) {
                        ProgressView()
                            .progressViewStyle(CircularProgressViewStyle(tint: .red))
                    }
                @unknown default:
                    Image("placeholder-image")
                }
            }
    }
}

Here is the code for view for respective properties including the image ..

import SwiftUI

struct PeopleCellView: View {
    let people: PeopleData
    var body: some View {
        HStack {
            if let url = URL(string: people.avatar ?? ""){
                PeopleAsyncImageView(url: url)
                    .frame(width: 150, height: 150)
                    .mask(RoundedRectangle(cornerRadius: 16))
            }
            VStack(alignment: .leading,spacing: 5){
                Text("First Name: " +  people.firstName)
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .font(.headline)

                Text("Last Name: " + (people.lastName ?? ""))
                    .frame(maxWidth: .infinity, alignment: .leading)


                Text("Email: " + (people.email ?? ""))
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .font(.subheadline)

            }
        }
    }
}
struct PeopleCellView_Previews: PreviewProvider {
    static var previews: some View {
        PeopleCellView(people: Constants.previewPeopleObj)
    }
}

Here is the details view code ..

import SwiftUI

struct PeopleDetailsView: View {
    
    @State var people: PeopleData
    var body: some View {
        VStack(alignment: .center) {

            Image(people.avatar ?? "")
            Text("First Name - \(people.firstName)")
            Text("Last Name: \(people.lastName ?? " ")")
            Text("Email: \(people.email ?? " ")")

        }
    }
}
struct PeopleDetailsView_Previews: PreviewProvider {
    static var previews: some View {
       PeopleDetailsView(people: Constants.previewPeopleObj)
    }
}

Here is the screenshot of the app ..

enter image description here

Here is the screenshot of the details view ..

enter image description here


Solution

  • Based on your code, the people.avatar is just a URL, which your list is loading into a custom PeopleAsyncImageView(url: url), which presumably downloads the image. While in details you are trying to use Image(people.avatar ?? "") - that is passing a URL to Image view, which does not accept URLs.

    So you have 2 options:

    1. Use PeopleAsyncImageView in your PeopleDetailsView view
    2. Or get your PeopleAsyncImageView to cache an image it downloaded (either on disk, or in memory) and pass it to PeopleDetailsView.

    The first solution is simpler, just copy how the image is loaded in your list:

    struct PeopleDetailsView: View {
        
        @State var people: PeopleData
        var body: some View {
            VStack(alignment: .center) {
    
                // Image(people.avatar ?? "") <-- remove this
                if let url = URL(string: people.avatar ?? ""){ // <-- add this
                    PeopleAsyncImageView(url: url)
                        .frame(width: 150, height: 150)
                        .mask(RoundedRectangle(cornerRadius: 16))
                }