Doing things series of articles is mainly to continue to continue their “T” strategy, but also represents the learning summary of the relevant content of bi design. This article is a reflection on implementing a selector component in a project.

preface

Someone once said that “a good product usually wins in some details”, which has been well verified in me. After completing a version of the selector design last year (see this article for details), we are now implementing a second version.

When I saw the design, I couldn’t help but marvel at how imaginative the designer was, completely abandoning the conventional selector design.

After checking with the UI, I immediately thought, “I don’t want to write it myself!” “But soon realized that there was probably no such open source component available. In short, I planted the seed of one of the most difficult dynamic effects of the whole project.

research

As expected, github tried to search picker, Swpier, slider and many other keywords related to selectors, but failed. I even tried to transform the amplification component in collectionView, but I found it was really ugly after some operation.

After this transformation, I found that the amplification effect of the middle view of collectionView is based on the dynamic change of the scale attribute of the occurrence cell, so I started to write my own.

thinking

Staring at the blueprints for a long time, mulling over the motion. Finally, I summed up the following ideas:

  • useUICollectionViewThe law of cosines doesscaleChange, you can find an open source component to do secondary development (the shortest time).
  • useUICollectionView, eachcellIt’s all the same size, the middle part to do the “magnifying glass” effect, put the wholecollectionViewMake the 3D conversion from a scroll wheel with depth, and each scroll is just modifying the content on the X axis, leaving the Z and Y axes unchanged (for best results).
  • useUIScrollViewTo make a “wheel cast” effect, you need to do everything yourself (the easiest implementation).

In fact, I spent most of my time on the first scheme, because the actual dynamic effect is exactly the same as the first scheme, but the cell is very small. However, as mentioned above, I tried to modify several open source components twice, but found that the effect was too miserable to look at, so I gave up; The second scheme is my own creation, because the dynamic effect is particularly like a roller perpendicular to the screen, but the students who have done 3D transformation also know that many parameters need to adjust, it is not worth the gain.

Best to use the simplest and most direct method, with UIScrollView hard build.

implementation

The first step

I had to get everything ready first, and I quickly wrote the code that arranges all the subviews into a scrollView.

private func initView(a) {
    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false
    addSubview(scrollView)
    var finalW: CGFloat = 0
    for index in 0..<pickCount {
        let inner = 10
        let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
        sv.backgroundColor = .lightGray
        scrollView.addSubview(sv)

        if index == pickCount - 1 {
            finalW = sv.right
        }
    }
    scrollView.contentSize = CGSize(width: finalW, height: 0)}Copy the code

The second step

Several views near the center of the screen need to be raised regularly. It took some time to find the parameters that pulled the middle view up and tweaked them.

private func initView(a) {
        
    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false 
    addSubview(scrollView)
    
    var finalW: CGFloat = 0
    for index in 0..<pickCount {
        
        // The spacing between subviews
        let inner = 10
        // sv for each subview
        let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
        sv.backgroundColor = .lightGray
        sv.tag = index + 100
        scrollView.addSubview(sv)
        
        // Whether the current subview is within the scope of the central region
        if abs(sv.centerX - centerX) < 5 {
            
            sv.pj_height = 18
            sv.pj_width = 2
            sv.backgroundColor = .black
            // Assign first to the central view
            centerView = sv
            
        } else if abs(sv.centerX - centerX) < 16 {
            
            sv.pj_height = 14
            sv.pj_width = 1
            
        } else if abs(sv.centerX - centerX) < 26 {
            
            sv.pj_height = 8
            sv.pj_width = 1
            
        } else {
            
            sv.pj_height = 4
            sv.pj_width = 1
            
        }
        
        sv.y = (scrollView.pj_height - sv.pj_height) * 0.5
        
        if index == pickCount - 1 {
            
            finalW = sv.right
            
        }
    }
    
    scrollView.contentSize = CGSize(width: finalW, height: 0)}Copy the code

The third step

The height of the middle area view needs to be calculated in real time while scrolling. Now that you have the view initialization criteria, you can just use it, just add the x-offset of the scrollView slide.

extension PJRulerPickerView: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        let offSetX = scrollView.contentOffset.x
        let _ = scrollView.subviews.filter {
            
            if abs($0.centerX - offSetX - centerX) < 5{$0.pj_height = 18
                $0.pj_width = 2
                $0.backgroundColor = .black
                
            } else if abs($0.centerX - offSetX  - centerX) < 16{$0.pj_height = 14
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            } else if abs($0.centerX - offSetX - centerX) < 26{$0.pj_height = 8
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            } else{$0.pj_height = 4
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            }

            $0.y = (scrollView.pj_height - $0.pj_height) * 0.5
            return true}}}Copy the code

The fourth step

Do this basically simple to complete the requirements, not complicated at all!! I really don’t know why we should spend half a day to find open source libraries to do secondary development.

While determining the dynamic effect to the UI, it is told that the left and right views cannot be “dragged”, which means turning off the spring effect, using the property scrollView.bounces = false.

The user was allowed to tap 100 times, but there was less scrolling to scroll because the “spring effect” was turned off. After some thought, I used some simple math to make the scrollView render more of the scrolling content occupied by the head and tail.

private func initView(a) {
    
    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
    addSubview(scrollView)
    scrollView.delegate = self
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false
    scrollView.bounces = false

    // The number occupied from the left to the center of the screen
    // 10.5 is the width of each subview + the left margin, adding 1 to add the first rendered center view
    startIndex = (Int(ceil(centerX / 10.5)) + 1)
    // The total number of subviews to render plus the number of heads and tails occupied
    pickCount += startIndex * 2
    
    var finalW: CGFloat = 0
    
    for index in 0..<pickCount {
        
        // The spacing between subviews
        let inner = 10
        // sv for each subview
        let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
        sv.backgroundColor = .lightGray
        scrollView.addSubview(sv)
        
        // Whether the current subview is within the scope of the central region
        if abs(sv.centerX - centerX) < 5 {
            
            sv.pj_height = 18
            sv.pj_width = 2
            sv.backgroundColor = .black
            // Assign first to the central view
            centerView = sv
            
        } else if abs(sv.centerX - centerX) < 16 {
            
            sv.pj_height = 14
            sv.pj_width = 1
            
        } else if abs(sv.centerX - centerX) < 26 {
            
            sv.pj_height = 8
            sv.pj_width = 1
            
        } else {
            
            sv.pj_height = 4
            sv.pj_width = 1
            
        }
        
        sv.y = (scrollView.pj_height - sv.pj_height) * 0.5
        
        if index == pickCount - 1 {
            
            finalW = sv.right
            
        }
    }
    
    scrollView.contentSize = CGSize(width: finalW, height: 0)}Copy the code

Step 5

Now that you’ve basically solved the UI problem, all you need to do is expose the number of dips the user makes. After thinking about it for a while, I came to the conclusion that counting the number of times the user currently dips the selector is actually counting how many times the middle view “goes black.” After figuring it out, I quickly wrote down the code:

private func initView(a) {
    
    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
    addSubview(scrollView)
    scrollView.delegate = self
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false
    scrollView.bounces = false

    // The number occupied from the left to the center of the screen
    startIndex = (Int(ceil(centerX / 10.5)) + 1)
    // The total number of subviews to render plus the number of heads and tails occupied
    pickCount += startIndex * 2
    
    var finalW: CGFloat = 0
    
    for index in 0..<pickCount {
        
        // The spacing between subviews
        let inner = 10
        // sv for each subview
        let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
        sv.backgroundColor = .lightGray
        sv.tag = index + 100
        scrollView.addSubview(sv)
        
        // Whether the current subview is within the scope of the central region
        if abs(sv.centerX - centerX) < 5 {
            
            sv.pj_height = 18
            sv.pj_width = 2
            sv.backgroundColor = .black
            // Assign first to the central view
            centerView = sv
            
        } else if abs(sv.centerX - centerX) < 16 {
            
            sv.pj_height = 14
            sv.pj_width = 1
            
        } else if abs(sv.centerX - centerX) < 26 {
            
            sv.pj_height = 8
            sv.pj_width = 1
            
        } else {
            
            sv.pj_height = 4
            sv.pj_width = 1
            
        }
        
        sv.y = (scrollView.pj_height - sv.pj_height) * 0.5
        
        if index == pickCount - 1 {
            
            finalW = sv.right
            
        }
    }
    
    scrollView.contentSize = CGSize(width: finalW, height: 0)}extension PJRulerPickerView: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        let offSetX = scrollView.contentOffset.x
        let _ = scrollView.subviews.filter {
            
            if abs($0.centerX - offSetX - centerX) < 5{$0.pj_height = 18
                $0.pj_width = 2
                $0.backgroundColor = .black
                
                // If the central view is not the same as the previous one, the central view has been replaced
                ifcenterView.tag ! = $0.tag {
                    
                    centerView = $0
                    // We can calculate the dial times here}}else if abs($0.centerX - offSetX  - centerX) < 16{$0.pj_height = 14
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
            } else if abs($0.centerX - offSetX - centerX) < 26{$0.pj_height = 8
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
            } else{$0.pj_height = 4
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
            }

            $0.y = (scrollView.pj_height - $0.pj_height) * 0.5
            return true}}}Copy the code

I use an intermediate variable as a reference to the intermediate view and tag it when I create the child view. After thinking about it for a while, I was influenced by the previous several times of thinking, which led to the calculation of how many times the user had touched the method and did some mathematical calculations without thinking. Finally, I did the following:

extension PJRulerPickerView: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        let offSetX = scrollView.contentOffset.x
        let _ = scrollView.subviews.filter {
            
            if abs($0.centerX - offSetX - centerX) < 5{$0.pj_height = 18
                $0.pj_width = 2
                $0.backgroundColor = .black
                
                // If this central view is not the last central view
                ifcenterView.tag ! = $0.tag {
                    
                    PJTapic.select()
                    centerView = $0
                    
                    // Number of user dials
                    print(Int(ceil($0.centerX / 10.5)) - startIndex)
                }
                
            } else if abs($0.centerX - offSetX  - centerX) < 16{$0.pj_height = 14
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            } else if abs($0.centerX - offSetX - centerX) < 26{$0.pj_height = 8
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            } else{$0.pj_height = 4
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            }

            $0.y = (scrollView.pj_height - $0.pj_height) * 0.5
            return true}}}Copy the code

While writing this article, I noticed a particularly silly place. I have already recorded the position represented by each subview in the tag. Why should I recalculate the position of the current middle view? After realizing this problem, I made some other changes and ended up with the following code for PJRulerPickerView:

//
// PJRulerPicker.swift
// PIGPEN
//
// Created by PJHubs on 2019/5/16.
// Copyright © 2019 PJHubs. All rights reserved
//

import UIKit

class PJRulerPickerView: UIView {
    
    /// get the dial count
    var moved: ((Int) - >Void)?
    /// The number of times it needs to be plucked
    var pickCount  = 0
    // Central view
    private var centerView = UIView(a)private var startIndex = 0
    
    override init(frame: CGRect) {
        
        super.init(frame: frame)
    }
    
    required init? (coder aDecoder:NSCoder) {
        
        fatalError("init(coder:) has not been implemented")}convenience init(frame: CGRect, pickCount: Int) {
        
        self.init(frame: frame)
        self.pickCount = pickCount
        initView()
        
    }
    
    private func initView(a) {
        
        let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
        addSubview(scrollView)
        scrollView.delegate = self
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false
        scrollView.bounces = false

        // The number occupied from the left to the center of the screen
        startIndex = (Int(ceil(centerX / 10.5)))
        // The total number of subviews to render plus the number of heads and tails occupied
        pickCount += startIndex * 2 + 1
        
        var finalW: CGFloat = 0
        
        for index in 0..<pickCount {
            
            // The spacing between subviews
            let inner = 10
            // sv for each subview
            let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
            sv.backgroundColor = .lightGray
            sv.tag = index + 100
            scrollView.addSubview(sv)
            
            // Whether the current subview is within the scope of the central region
            if abs(sv.centerX - centerX) < 5 {
                
                sv.pj_height = 18
                sv.pj_width = 2
                sv.backgroundColor = .black
                // Assign first to the central view
                centerView = sv
                
            } else if abs(sv.centerX - centerX) < 16 {
                
                sv.pj_height = 14
                sv.pj_width = 1
                
            } else if abs(sv.centerX - centerX) < 26 {
                
                sv.pj_height = 8
                sv.pj_width = 1
                
            } else {
                
                sv.pj_height = 4
                sv.pj_width = 1
                
            }
            
            sv.y = (scrollView.pj_height - sv.pj_height) * 0.5
            
            if index == pickCount - 1 {
                
                finalW = sv.right
                
            }
        }
        
        scrollView.contentSize = CGSize(width: finalW, height: 0)}}extension PJRulerPickerView: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        let offSetX = scrollView.contentOffset.x
        let _ = scrollView.subviews.filter {
            
            if abs($0.centerX - offSetX - centerX) < 5{$0.pj_height = 18
                $0.pj_width = 2
                $0.backgroundColor = .black
                
                // If this central view is not the last central view
                ifcenterView.tag ! = $0.tag {
                    
                    PJTapic.select()
                    centerView = $0
                    
// moved? (Int(ceil($0.centerx / 10.5)) - startIndex)moved? ($0.tag - 100 - startIndex)
// print($0.tag - 100 - startIndex)}}else if abs($0.centerX - offSetX  - centerX) < 16{$0.pj_height = 14
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            } else if abs($0.centerX - offSetX - centerX) < 26{$0.pj_height = 8
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            } else{$0.pj_height = 4
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            }

            $0.y = (scrollView.pj_height - $0.pj_height) * 0.5
            return true}}}Copy the code

conclusion

After completing the PJRulerPickerView component, I realized that I should have carefully deduced the whole problem in my mind to see what the real core problem is, instead of spending half a day aimlessly looking for open source component library like I did before.

This component wasn’t difficult, but it made a big difference to me and made me realize that I shouldn’t sell myself short.

PJ’s path to iOS development