If we want to have a circle with no moved
border effect as we can see doing it by using ZStack { Circle().fill(), Circle().stroke }
I prepared something like below:
First step
We are creating a new Shape
struct CircleShape: Shape {
// MARK: - Variables
var radius: CGFloat
func path(in rect: CGRect) -> Path {
let centerX: CGFloat = rect.width / 2
let centerY: CGFloat = rect.height / 2
var path = Path()
path.addArc(center: CGPoint(x: centerX, y: centerY), radius: radius, startAngle: Angle(degrees: .zero)
, endAngle: Angle(degrees: 360), clockwise: true)
return path
}
}
Second step
We are creating a new ButtonStyle
struct LikeButtonStyle: ButtonStyle {
// MARK: Constants
private struct Const {
static let yHeartOffset: CGFloat = 1
static let pressedScale: CGFloat = 0.8
static let borderWidth: CGFloat = 1
}
// MARK: - Variables
var radius: CGFloat
var isSelected: Bool
func makeBody(configuration: Self.Configuration) -> some View {
ZStack {
if isSelected {
CircleShape(radius: radius)
.stroke(Color.red)
.animation(.easeOut)
}
CircleShape(radius: radius - Const.borderWidth)
.fill(Color.white)
configuration.label
.offset(x: .zero, y: Const.yHeartOffset)
.foregroundColor(Color.red)
.scaleEffect(configuration.isPressed ? Const.pressedScale : 1.0)
}
}
}
Last step
We are creating a new View
struct LikeButtonView: View {
// MARK: - Typealias
typealias LikeButtonCompletion = (Bool) -> Void
// MARK: - Constants
private struct Const {
static let selectedImage = Image(systemName: "heart.fill")
static let unselectedImage = Image(systemName: "heart")
static let textMultiplier: CGFloat = 0.57
static var textSize: CGFloat { 30 * textMultiplier }
}
// MARK: - Variables
@State var isSelected: Bool = false
private var radius: CGFloat = 15.0
private var completion: LikeButtonCompletion?
init(isSelected: Bool, completion: LikeButtonCompletion? = nil) {
_isSelected = State(initialValue: isSelected)
self.completion = completion
}
var body: some View {
ZStack {
Button(action: {
withAnimation {
self.isSelected.toggle()
self.completion?(self.isSelected)
}
}, label: {
setIcon()
.font(Font.system(size: Const.textSize))
})
.buttonStyle(LikeButtonStyle(radius: radius, isSelected: isSelected))
}
}
// MARK: - Private methods
private func setIcon() -> some View {
isSelected ? Const.selectedImage : Const.unselectedImage
}
}
Output (Selected and unselected state):

