Search code examples
swiftswiftuislider

Before-After Image Slider in SwiftUI


How to implement a before-after image slider in SwiftUI? I've searched the entire internet, but the problem remains unsolved. I have an implementation, but it doesn't work correctly. When you start moving the slider, it doesn't align properly with the photo. The second problem is that the mask doesn't work correctly. I have a capsule with text over the photo, and there is a capsule on the 'before' label, but for some reason, it's missing on the 'after' label.

import SwiftUI

struct ContentView: View {
    
    @State private var location: CGPoint = CGPoint(x: 0, y: 0)
    @State private var maskWidth: CGFloat = 0.0
    
    @State var startPoint: CGFloat = 0
    @State var endPoint: CGFloat = 0
    @State var yPoint: CGFloat = 0
    
    var sliderWidth: CGFloat = 30
    var containerWidth: CGFloat = 400
    var containerHeight: CGFloat = 300
    
    var body: some View {
        
        ZStack {
            ZStack {
                
                Image(.boyPic)
                    .resizable()
                    .frame(width: containerWidth, height: containerHeight)
                    .clipped()
                    .overlay {
                        ZStack {
                            Capsule()
                                .frame(width: 64, height: 30)
                                .foregroundStyle(Color.black.opacity(0.6))
                            
                            Text(BeforeAfterType.before.rawValue)
                                .foregroundStyle(Color.whiteColor)
                                .fonts(.medium, .smallTitleSize13)
                        }
                        .horAlig(.leading)
                        .verAlig(.top)
                        .padding(ARSizesType.smallPaddingSize.sizes)
                    }
                
                Image(.boyPic)
                    .resizable()
                    .frame(width: containerWidth, height: containerHeight)
                    .clipped()
                    .overlay {
                        ZStack {
                            Capsule()
                                .frame(width: 64, height: 30)
                                .foregroundStyle(Color.black.opacity(0.6))
                            
                            Text(BeforeAfterType.after.rawValue)
                                .foregroundStyle(Color.whiteColor)
                                .fonts(.medium, .smallTitleSize13)
                        }
                        .horAlig(.trailing)
                        .verAlig(.top)
                        .padding(ARSizesType.smallPaddingSize.sizes)
                    }
                    .mask(mask)
                
            }
            
            Slider
        }
        .frame(width: containerWidth, height: containerHeight)
        .onAppear {
            yPoint = containerHeight/2
            location = CGPoint(x: containerWidth/2, y: yPoint)
            maskWidth = containerWidth/2
            endPoint = containerWidth
        }
        
    }
    
    var dragAction: some Gesture {
        DragGesture()
            .onChanged { value in
                updateDragView(point: value.location)
                updateMaskView(point: value.translation)
            }
            .onEnded { value in
                setInitialPosition()
            }
    }
    
    var mask: some View {
        HStack {
            Spacer()
            Rectangle()
                .mask(Color.black)
                .frame(width: maskWidth, height: containerHeight)
        }
    }
    
    var Slider: some View {
        
        ZStack {
            Rectangle()
                .fill(Color.white)
                .frame(width: 2)
            
            Image(.sliderButton)
                .resizable()
                .scaledToFill()
                .foregroundColor(.white)
                .frame(width: 55, height: 35)
                .verAlig(.bottom, 50)
            
        }
        .position(location)
        .gesture(dragAction)
        .shadow(radius: 4)
    }
    
    func updateDragView(point: CGPoint) {
        let locX = point.x
        if locX > startPoint && locX < endPoint {
            self.location = CGPoint(x: point.x, y: yPoint)
        }
    }
    
    func updateMaskView(point: CGSize) {
        let width = -(point.width)
        let newWidth = ((containerWidth/2) + width)
        if newWidth > 0 {
            maskWidth = ((containerWidth/2) + width)
        } else {
            setInitialPosition()
        }
    }
    
    func setInitialPosition() {
        withAnimation {
            location = CGPoint(x: containerWidth/2, y: yPoint)
            maskWidth = containerWidth/2
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Solution

  • Demo

    Your original code is so long to follow, but here is what you should do:

    struct BeforeAfterSlider: View {
        @State private var percentage = 0.0
    
        var body: some View {
            ZStack { // 👈 Stacks image on top of each other
                Image(.before)
    
                Image(.after)
                    .mask { // 👈 Mask the top one
                        Rectangle() // 👈 Use the desired shape for the mask
                            .scaleEffect(
                                CGSize(
                                    width: percentage, // 👈 Control the width of the mask
                                    height: 1.0
                                ),
                                anchor: .leading
                            )
                    }
            }
            .overlay(alignment: .bottom) {
                Slider(value: $percentage, in: 0...1) // 👈 Connect the mask to your slider
            }
        }
    }