In ZStack views are independent on each other and stack fits (if does not have own frame) to biggest view. Also order in ZStack can be modified by using .zIndex modifier. All views are in ZStack coordinate space.
In .overlay case the view inside overlay always bound to parent view and always above parent view (ie. zIndex does not play any role). Also overlaid views are in parent view coordinate space.
The most visible difference is if to make views different size and apply clipping, ie
struct TestZStack: View {
var body: some View {
ZStack(alignment: .bottom) {
Image(systemName: "folder")
.font(.system(size: 55, weight: .thin))
Text("❤️").font(.system(size: 55, weight: .thin))
}
.clipped()
}
}
gives

struct TestOverlay: View {
var body: some View {
Image(systemName: "folder")
.font(.system(size: 55, weight: .thin))
.overlay(Text("❤️").font(.system(size: 55, weight: .thin)), alignment: .bottom)
.clipped()
}
}
gives
