drago
drago

Reputation: 1303

Placing background image and text/icon in a button in SwiftUI?

I'm trying to achieve something like this in SwiftUI:

enter image description here

I can create the button with rounded corners and I can also place the text in the center of it but that's about it.

I need to be able to place background image and the footer title/text and the icon at the top!

This is what I have so far:

                            Button(action: {
                                    //self.dismiss?()
            
                                    
                                })
                                {
                               

                                    HStack(spacing: 10) {
                                        Image(systemName: "pencil").renderingMode(.template)
                                        Text(audio.name)
                                            //.font(.headline)
                                            .frame(width: 160, height: 200)
                                        
                                            .background(Color.gray)
                                            .addBorder(Color.white, width: 1, cornerRadius: 10)
                                    }
                                    
                    
                                    
                                }

When I try my code, I get a button with rounded corner and the text in the middle but the Image(systemName: "pencil") is outside of the button for some reason!

Can someone please guide me how to achieve this?

It might be worth mentioning that the background images are coming from a remote server.

Upvotes: 3

Views: 2097

Answers (3)

Hans Rietmann
Hans Rietmann

Reputation: 416

First, to handle the backend that loads the image, create an Observable object that handles states and networking.

import Combine



class ImageManager: ObservableObject {

    // Property that fires an update onto the interface when it has any changes to its value
    @Published var image: UIImage? = nil

    init(link: String) {
        // Standard way of loading data from a link.
        guard let url = URL(string: link) else { return }
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            if let error = error {
                // handle the error here !
                print(error)
            } else if let data = data {
                // Never forget to update on the main thread ! 😁
                DispatchQueue.main.async {
                    self.image = UIImage(data: data)
                }
            }
        }
        task.resume()
    } 
}

Second, build the Image view that handles the communication with the Observable object as it follows :

struct ImageLoader: View {

     // The property wrapper '@ObservedObject' makes our ImageLoader change its interface according to updates received from our ImageManager
     @ObservedObject private var manager: ImageManager

     init(link: String) {
         manager = ImageManager(link: link)
     }

     var body: some View {
         Image(uiImage: manager.image ?? UIImage())
            .resizable()
            .aspectRatio(contentMode: .fill)
            // Make the view greyish as long there is no image loaded to present
            .redacted(reason: manager.image == nil ? .placeholder:[])
    }
}

And third, build the final view. You can use overlay to add views on top of your card as well as a

.frame(maxWidth:, maxHeight, alignment:)

to build your overlay views efficiently without having too many additional views. 😉

struct MovieCard: View {

    @State var thumbnailLink = "https://media.senscritique.com/media/000010045541/source_big/Tomb_Raider.png"

     var body: some View {
        ImageLoader(link: thumbnailLink)
            .frame(width: 200, height: 200)
            // Overlay is a very simple way to add content on top of your view ! 😁
            .overlay(
                 VStack {
                     // Using the system heart icon see https://developer.apple.com/sf-symbols/
                     Image(systemName: "suit.heart.fill")
                        .font(.title3)
                        .padding()
                        // Using maxWidth, maxHeight and alignement is usefull to not use additionnal views in your code (such as spacers, stacks, etc…)
                        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
                        .foregroundColor(.red)
                
                     Text("Movie title")
                        .font(Font.body.bold())
                        .foregroundColor(.white)
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background(Color.red.opacity(0.7))
                }
            )
            .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
            .shadow(color: Color.black.opacity(0.3), radius: 6, x: 0, y: 3)
            .onTapGesture {
                // Handle the action when the card is touched
            }
        }
    }

enter image description here

Upvotes: 4

LuLuGaGa
LuLuGaGa

Reputation: 14418

There are quite a few elements to get it right so I have placed comments below in relevant places.

Button(action: { }) {
    Image("Tomb_Raider")
        //declare your image as resizable, otherwise it will keep its original size
        .resizable()
        //declare the frame of the image (i.e. the size you want it to resize to)
        .frame(width: 160, height: 200)
        // place the red rectangle with text in an overlay
        // as opposed to HStack where elements are side by side
        // here the image will be placed under the rest
        .overlay(
            //This HStack places the text in the middle with some padding
            HStack {
                Spacer()
                Text("Title goes Here")
                    .font(.headline)
                    .foregroundColor(.white)
                    .padding(.vertical)
                 Spacer()
            }   
                //set the background of the text to be semitransparent red
                .background(Color.red.opacity(0.6)),
                //this defines the alignment of the overlay
                alignment: .bottom)
        //clip everything to get rounded corners
        .clipShape(RoundedRectangle(cornerRadius: 10))
    }
        

And that's how the end result looks:

enter image description here

Upvotes: 2

ARR
ARR

Reputation: 2308

Move the frame and background modifiers to hstack:

            HStack {
                        Image(systemName: "pencil").renderingMode(.template)
                        Spacer()
                        Text("title goes here")
                        Spacer()
                }.frame(width: 160, height: 200)
                
                .background(Color.gray)
            }

You can also add a Spacer() between Image and Text and a Spacer() after Text, to align everything correctly

Upvotes: 2

Related Questions