swiftswiftuialignment

Drop down menu button is moving around frame when tapped


I am new to SwiftUI. I have a project where the element called tile needs to have a dropdown menu button in right top corner. I have done that part and add the dropdown menu. But... Whenever I tap the button it swings to the side to be in the center with the content of the dropdown menu -> Screenrecording here

Also when the text displayed is shorter the dotted button disappears on tap, if I understand correctly it moves out of frame, righht? How can I prevent this?

Whhat am I missing, help will be much appreciated.

The code for that tile and menu below:

import SwiftUI

// just for testing purpose
let iconName = "ellipsis.rectangle"

protocol TileProtocol {
    init (
        icon: String,
        text: String,
        action: TileAction?,
        hasMenuButton: Bool,
        scaler: CGFloat
    )
}

struct TileAction {
    let title: String
    let alignment: Alignment
    let callback:(() -> Void)
}

struct TileWithMenu: TileProtocol, View {
    let icon: String
    let text: String
    let action: TileAction?
    let hasMenuButton: Bool
    let scaler: CGFloat
    
    var body: some View{
        HStack(alignment: .top, spacing: 0) {
            Image(icon)
                .resizable()
                .scaledToFit()
                .frame(width: scaler, alignment: .leading)
                .frame(maxWidth: 30, maxHeight: 30)
                .padding(.top, 4)
                .padding(.trailing, 10)
            
            HStack(alignment: .center,spacing: 0) {
                VStack(alignment: .leading,spacing: 16) {
                    Text(text)
                        .font(.headline)
                    
                    if let action, action.alignment == .bottom {
                        VStack {
                            Button(action.title) {
                                action.callback()
                            }
                        }
                    }
                }
            }
        }
        .overlay(alignment: Alignment(horizontal: .trailing, vertical: .top)) {
            HStack(alignment: .lastTextBaseline, spacing: 10){
                if hasMenuButton {
                    TileMenuButton(
                        options: [
                            TileAction(title: "Postpone for 24h", alignment: .trailing, callback: {
                                print("24h")
                            }),
                            TileAction(title: "Postpone for 24h", alignment: .trailing, callback: {
                                print("1 week")
                            }),
                            TileAction(title: "Cancel", alignment: .trailing, callback: {
                                print("cancel")
                            })
                        ],
                        iconScaling: scaler
                    )
                }
            }
        }
    }
}

struct TileMenuButton: View {
    @State private var isVisible: Bool = false
    let options: [TileAction]
    let iconScaling: CGFloat
    
    var body: some View {
        VStack {
            Button(action: {
                withAnimation {
                    isVisible.toggle()
                }
            }) {
                Image(iconName)
                    .resizable()
                    .scaledToFit()
                    .frame(width: iconScaling)
            }
            .padding(2)
            .background(.white)
            
            if isVisible {
                VStack {
                    ForEach(0..<options.count, id: \.self) { n in
                        Button(action: options[n].callback) {
                            Text(options[n].title)
                                .font(.caption)
                                .padding(.top, 1)
                                .background(.white)
                        }
                    }
                }
                .transition(.asymmetric(insertion: .scale, removal: .opacity))
            }
        }
    }
}

// MARK: PREVIEW
struct TileWithMenu_Previews: PreviewProvider {
    static var previews: some View {
        TileWithMenu(
            icon: iconName,
            text: "Some example of a very long text. Some example of a very long text. Some example of a very long text",
            action: TileAction(title: "A Button", alignment: .bottom, callback: {
                print("A Button tap")
            }),
            hasMenuButton: true,
            scaler: 20
        )
    }
}

And here is how I use it

struct ContentView: View {
    var body: some View {
        VStack {
            TileWithMenu(
                icon: iconName,
                text: "Some example of a very long text. Some example of a very long text. Some example of a very long text",
                action: TileAction(title: "A Button", alignment: .bottom, callback: {
                    print("A Button tap")
                }),
                hasMenuButton: true,
                scaler: 20
            )
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

I was searching for the solution on line. Haven't found one so far.


Solution

  • To resolve the sideways movement, try adding (alignment: .trailing) to the top-level VStack of TileMenuButton:

    // TileMenuButton
    
    VStack(alignment: .trailing) { // 👈 HERE
        Button //...
    
        if isVisible {
            // ...
        }
    }
    

    Animation

    To resolve the issue of the menu icon disappearing when the text is short, try adding .fixedSize(horizontal: false, vertical: true) to the HStack inside the overlay of TileWithMenu:

    // TileWithMenu
    
    HStack(alignment: .top, spacing: 0) {
        // ...
    }
    .overlay(alignment: Alignment(horizontal: .trailing, vertical: .top)) {
        HStack(alignment: .lastTextBaseline, spacing: 10) {
            // ...
        }
        .fixedSize(horizontal: false, vertical: true) // 👈 HERE
    }