Search code examples
iosswiftuiscrollviewuiscrollviewdelegate

Zoom only horizontally UIView in UIScrollView


I have tried some solutions from other posts but I haven't been able to solve the question I am going to post.

I have a custom UIView that draws a SoundWave and it contains a UIScrollView. The UIScrollView contains a main UIView that contains two other custom UIViews (marker left and right) and UIImage view where i render the sound wave.

private func initialize(){

    print("\(logClassName) initialize in Frame: \(frame)")

    //ScrollView
    addSubview(scrollView)
    scrollView.constraintToSuperViewEdges()


    //WaveView
    scrollView.addSubview(waveView)
    waveView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0).isActive = true
    waveView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 0).isActive = true
    waveView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0).isActive = true
    waveView.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true

    waveViewWidthConstraint = waveView.widthAnchor.constraint(equalToConstant: 1000)
    NSLayoutConstraint.activate([waveViewWidthConstraint])
    scrollView.contentSize = CGSize(width: waveViewWidthConstraint.constant, height: 0)

    //SoundWaveImageView
    waveView.addSubview(soundWaveImageView)
    soundWaveImageView.constraintToSuperViewEdges()


    //WaveView markers
    //TimeMarker
    waveView.addSubview(timeMarkerView)
    timeMarkerView.topAnchor.constraint(equalTo: waveView.topAnchor).isActive = true
    timeMarkerView.bottomAnchor.constraint(equalTo: waveView.bottomAnchor).isActive = true
    timeMarkerView.widthAnchor.constraint(equalToConstant: 1).isActive = true
    timerMarkerViewLeadingContraint = timeMarkerView.leadingAnchor.constraint(equalTo: waveView.leadingAnchor, constant: 20)
    NSLayoutConstraint.activate([timerMarkerViewLeadingContraint])


    //LeftMarker
    waveView.addSubview(leftMarkerView)
    leftMarkerView.topAnchor.constraint(equalTo: waveView.topAnchor).isActive = true
    leftMarkerView.bottomAnchor.constraint(equalTo: waveView.bottomAnchor).isActive = true
    leftMarkerView.widthAnchor.constraint(equalToConstant: leftMarkerView.triangleWidth).isActive = true
    leftMarkerViewLeadingConstraint = leftMarkerView.leadingAnchor.constraint(equalTo: waveView.leadingAnchor, constant: 0)
    NSLayoutConstraint.activate([leftMarkerViewLeadingConstraint])


    //RightMarker
    waveView.addSubview(rightMarkerView)
    rightMarkerView.topAnchor.constraint(equalTo: waveView.topAnchor).isActive = true
    rightMarkerView.bottomAnchor.constraint(equalTo: waveView.bottomAnchor).isActive = true
    rightMarkerView.widthAnchor.constraint(equalToConstant: rightMarkerView.triangleWidth).isActive = true
    rightMarkerViewLeadingConstraint = rightMarkerView.trailingAnchor.constraint(equalTo: waveView.leadingAnchor, constant: 50)
    NSLayoutConstraint.activate([rightMarkerViewLeadingConstraint])

}

As seen, the main UIView is constraint to the edges of the UIScrollView and the width constraint is the total length which changes according the needs. The function responsible for that is the following:

private func updateWidthConstraintValue(_ newWidth:CGFloat){

    waveViewWidthConstraint.constant = newWidth
    scrollView.contentSize = CGSize(width: waveViewWidthConstraint.constant, height: 0)

}

When implementing the zoom for the UIScrollView it works but it zooms both vertically and horizontally (as expected) but I would like to implement only horizontally. I have this so far:

func scrollViewDidZoom(_ scrollView: UIScrollView) {

    print("\(logClassName) TEST -> scrollView did zoom \(scrollView.zoomScale)")
    expandHorizontally(withScale: scrollView.zoomScale)
    //scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0)
    //scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
}

func viewForZooming(in scrollView: UIScrollView) -> UIView? {

    return waveView

}

private func expandHorizontally(withScale scale:CGFloat){

    let scaledWidth = currentWaveViewWidth * scale
    print("\(logClassName) TEST -> scaledWidth = \(scaledWidth)")

    updateWidthConstraintValue(scaledWidth)
    // Adjust content size and content offset
    scrollView.contentSize = CGSize(width: waveViewWidthConstraint.constant,
                                    height: 0);
    scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x,
                                       y: scrollView.contentOffset.y);

}

The UI Variables are declared like that:

lazy private (set) var scrollView:UIScrollView = {

    let rtView = UIScrollView()

    rtView.translatesAutoresizingMaskIntoConstraints = false
    rtView.backgroundColor = .clear

    rtView.bounces = false
    rtView.bouncesZoom = false

    rtView.minimumZoomScale = 1
    rtView.maximumZoomScale = 3

    rtView.delegate = self

    return rtView

}()

private (set) lazy var waveView:UIView = {

    let rtView = UIView()

    rtView.translatesAutoresizingMaskIntoConstraints = false
    rtView.backgroundColor = .clear
    rtView.addGestureRecognizer(UITapGestureRecognizer(target: self,
                                                       action: #selector(waveViewDidTouch)))

    return rtView

}()
private (set) var waveViewWidthConstraint = NSLayoutConstraint()

private (set) lazy var soundWaveImageView:UIImageView = {

    let rtView = UIImageView()

    rtView.translatesAutoresizingMaskIntoConstraints = false

    return rtView

}()

private (set) lazy var timeMarkerView:UIView = {
    let rtView = UIView()

    rtView.translatesAutoresizingMaskIntoConstraints = false
    rtView.backgroundColor = UIColor.AppColors.defaultTint

    return rtView
}()
private var timerMarkerViewLeadingContraint = NSLayoutConstraint()

private (set) lazy var leftMarkerView:MarkerView = {

    let rtView = MarkerView(title: AppHelper.printLocalized(withKey: "messages.start", targetSpecific: false), markerDirection: .right)

    rtView.translatesAutoresizingMaskIntoConstraints = false
    rtView.delegate = self

    return rtView

}()
private var leftMarkerViewLeadingConstraint = NSLayoutConstraint()

private (set) lazy var rightMarkerView:MarkerView = {

    let rtView = MarkerView(title: AppHelper.printLocalized(withKey: "messages.end", targetSpecific: false), markerDirection: .left)

    rtView.translatesAutoresizingMaskIntoConstraints = false
    rtView.delegate = self

    return rtView

}()
private var rightMarkerViewLeadingConstraint = NSLayoutConstraint()

I have tried to put all the relevant information as it is a big class.


Solution

  • I have figure out a temporary solution thank you to LaurentMaquet. At some point I will post the final soundWaveView as it is in development at the moment.

    The changes made are:

    1- Create a custom class for soundWave.

    class WaveView:UIView{
        var unzoomedViewHeight: CGFloat = 0
    
        override var transform: CGAffineTransform{
            get{ return super.transform }
            set{
                var t = newValue
                t.d = 1.0
                t.ty = (1.0 - t.a) * unzoomedViewHeight/2
                super.transform = t
            }
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            unzoomedViewHeight = frame.size.height
            print("\(logClassName) did layout to \(AppHelper.traitStatus) -> frame = \(frame)")
    
        }
    
    }
    

    2- Make my soundView var be a SoundView

    private (set) lazy var waveView:WaveView = {
    
        let rtView = WaveView()
    
        rtView.translatesAutoresizingMaskIntoConstraints = false
        rtView.backgroundColor = .clear
        rtView.addGestureRecognizer(UITapGestureRecognizer(target: self,
                                                           action: #selector(waveViewDidTouch)))
    
        return rtView
    
    }()
    

    Result

    enter image description here

    enter image description here