Search code examples
iosswiftswiftui

Image offset in swiftui


I keep learning swiftui in my free time and now I am making an app with API calls and a List, everything is fine until I need to make an offset of an image, I can do it but the image gets cut, here you have an image to see it.

This is what I have

enter image description here

This is what I want:

enter image description here

Why is the image cut off?

Here is my code:

PokemonItem

struct PokemonItem:View {
let pokemon: ApiNetwork.Pokemon
var body: some View {
    HStack {
        PokemonItemLeft(pokemon: pokemon)
        Spacer()
        PokemonItemRight(pokemon: pokemon)
    }.frame(height: 115)
     .background(colorForPokemonType(type: pokemon.details?.types.first?.type.name ?? ""))
     .cornerRadius(16)
     .listRowBackground(Color.clear)
     .padding(.bottom, 16)
    
     }
}

PokemonItemLeft

struct PokemonItemLeft: View {
let pokemon: ApiNetwork.Pokemon
var body: some View {
    VStack(alignment: .leading) {
        Image("DotsImage")
            .resizable()
            .scaledToFit()
            .frame(width: 100)
            .cornerRadius(8)
            .padding(.leading, 64)
        if let id = pokemon.details?.id {
            Text(formatPokemonID(id))
                .font(.caption)
                .foregroundColor(Color.pokemonId)
                .bold()
        } else {
            Text("ID: Unknown")
        }
        Text(pokemon.name.capitalized).font(.title2).foregroundColor(Color.white).bold()
    }.padding(.leading, 20)
}
}

PokemonItemRight

struct PokemonItemRight: View {
let pokemon: ApiNetwork.Pokemon

var body: some View {
        VStack {
            AsyncImage(url: URL(string: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(pokemon.details?.id ?? 0).png")) { image in
                image
                    .resizable()
                    .scaledToFit()
                    .frame(width: 130, height: 130)
                    .cornerRadius(8)
                    .padding(.trailing, 18)
                    .offset(y: -20)
                    .zIndex(1)
            } placeholder: {
                ProgressView()
            }
        }
        .padding(.leading, 20)
    }
}

This is why I am using ItemLeft and ItemRight

enter image description here

This is the function to change the background color

func colorForPokemonType(type: String) -> Color {
let cardColors: [String: Color] = [
    "grass": Color(red: 139/255, green: 190/255, blue: 138/255),
    "fire": Color(red: 255/255, green: 167/255, blue: 86/255),
    "water": Color(red: 88/255, green: 171/255, blue: 246/255),
    "poison": Color(red: 159/255, green: 110/255, blue: 151/255),
    "normal": Color(red: 181/255, green: 185/255, blue: 196/255),
    "bug": Color(red: 139/255, green: 214/255, blue: 116/255),
    "flying": Color(red: 116/255, green: 143/255, blue: 201/255),
    "electric": Color(red: 242/255, green: 203/255, blue: 85/255),
    "ground": Color(red: 247/255, green: 133/255, blue: 81/255),
    "fairy": Color(red: 235/255, green: 168/255, blue: 195/255),
    "fighting": Color(red: 208/255, green: 65/255, blue: 100/255),
    "psychic": Color(red: 234/255, green: 93/255, blue: 96/255),
    "rock": Color(red: 186/255, green: 171/255, blue: 130/255),
    "ghost": Color(red: 85/255, green: 106/255, blue: 174/255),
    "ice": Color(red: 97/255, green: 206/255, blue: 192/255),
    "dragon": Color(red: 15/255, green: 106/255, blue: 192/255)
]

return cardColors[type] ?? Color.gray

}

Here is the small problem with the first item of the list

enter image description here

Thanks a lot!


Solution

  • The reason your Pokemon is getting cropped because it's inside a HStack. HStack and VStack arranges its subviews horizontally and vertically respectively.

    To fix this, think your card as layers, base layer being the background, first layer being the pokemon image. Using ZStack you can achieve required results. The reason that ZStack works is that it "overlays" its subviews.

    Have written sample code below with some hardcoded values for the same.

    ContentView

    for testing purposes

    struct ContentView: View {
        var body: some View {
            PokemonItem()
        }
    }
    

    PokemonItem

    It has 2 layers: one base layer and one content layer

    struct PokemonItem: View {
        var body: some View {
            ZStack {
                //base card
                colorForPokemonType(type: "grass")
                    .cornerRadius(16)
                
                //content
                PokemonItemDetails()
            }
            .frame(height: 115)
            .listRowBackground(Color.clear)
            .padding(.bottom, 16)
        }
    }
    

    PokemonItemDetails

    A wrapper for content that needs to be displayed on the Card.

    struct PokemonItemDetails: View {
        var body: some View {
            HStack {
                PokemonItemLeft()
                Spacer()
                PokemonItemRight(id: 1)
            }
        }
    }
    

    PokemonItemLeft

    struct PokemonItemLeft: View {
        var body: some View {
            VStack(alignment: .leading) {
                Spacer()
                Image("DotsImage")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 100)
                    .cornerRadius(8)
                    .padding(.leading, 64)
                Text("#001")
                    .font(.caption)
                    .foregroundColor(Color.black)
                    .bold()
                Text("bulbasaur".capitalized)
                    .font(.title2)
                    .foregroundColor(Color.white)
                    .bold()
                    .padding(.bottom, 10)
            }.padding(.leading, 20)
        }
    }
    

    PokemonItemRight

    struct PokemonItemRight: View {
        let id: Int
        var body: some View {
            HStack {
                Spacer()
                AsyncImage(url: URL(string: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(id).png")) { image in
                    image
                        .resizable()
                        .scaledToFit()
                        .frame(width: 130, height: 130)
                        .cornerRadius(8)
                        .padding(.trailing, 18)
                        .offset(y: -20)
                        .zIndex(1)
                } placeholder: {
                    ProgressView()
                }
            }
            .padding(.leading, 20)
        }
    }
    

    result:

    pokemon card

    PS: to get extact positioning of dots, you'll just need to tweak some paddings and it should work fine.

    Hope this solves your query.