0

I want to replicate the Apple Map App Buttons as seen here: Screenshot

This is the code so far:

class CustomView: UIImageView {
init(frame: CGRect, corners: CACornerMask, systemName: String) {
    super.init(frame: frame)
    self.createBorders(corners: corners)
    self.createImage(systemName: systemName)
}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

func createBorders(corners: CACornerMask) {
    self.contentMode = .scaleAspectFill
    self.clipsToBounds = false
    self.layer.cornerRadius = 20
    self.layer.maskedCorners = corners
    self.layer.masksToBounds = false
    self.layer.shadowOffset = .zero
    self.layer.shadowColor = UIColor.gray.cgColor
    self.layer.shadowRadius = 20
    self.layer.shadowOpacity = 0.2
    self.backgroundColor = .white
    let shadowAmount: CGFloat = 2
    let rect = CGRect(x: 0, y: 2, width: self.bounds.width + shadowAmount * 0.1, height: self.bounds.height + shadowAmount * 0.1)
    self.layer.shadowPath = UIBezierPath(rect: rect).cgPath

}

func createImage(systemName: String) {
    let image = UIImage(systemName: systemName)!
    let renderer = UIGraphicsImageRenderer(bounds: self.frame)
    let renderedImage = renderer.image { (_) in
        image.draw(in: frame.insetBy(dx: 30, dy: 30))
    }

And use it like this:

let size = CGSize(width: 100, height: 100)
let frame = CGRect(origin: CGPoint(x: 100, y: 100), size: size)

let infoView = CustomView(frame: frame, corners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], systemName: "info.circle")
let locationView = CustomView(frame: frame, corners: [], systemName: "location")
let twoDView = CustomView(frame: frame, corners: [.layerMinXMaxYCorner, .layerMaxXMaxYCorner], systemName: "view.2d")
let binocularsView = CustomView(frame: frame, corners: [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner], systemName: "binoculars.fill")

let stackView = UIStackView(arrangedSubviews: [infoView, locationView, twoDView, binocularsView])
stackView.frame = CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: 100, height: 400))
stackView.distribution = .fillEqually
stackView.axis = .vertical
stackView.setCustomSpacing(20, after: twoDView)
view.addSubview(stackView)

Which makes it look like this:

MY Screenshot

AND: There aren't the thin lines between each Icon of the stackView :/

Thanks for your help!

1 Answers1

0

Not sure what's going on, because when I ran you code as-is I did not get the stretched images you've shown.

To get "thin lines between each Icon", you could either modify your custom image view to add the lines to the middle icon image, or, what might be easier, would be to use a 1-point height UIView between each icon.

Also, you'll be better off using auto-layout.

See if this gives you the desired result:

class CustomView: UIImageView {
    init(frame: CGRect, corners: CACornerMask, systemName: String) {
        super.init(frame: frame)
        self.createBorders(corners: corners)
        self.createImage(systemName: systemName)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func createBorders(corners: CACornerMask) {
        self.contentMode = .scaleAspectFill
        self.clipsToBounds = false
        self.layer.cornerRadius = 20
        self.layer.maskedCorners = corners
        self.layer.masksToBounds = false
        self.layer.shadowOffset = .zero
        self.layer.shadowColor = UIColor.gray.cgColor
        self.layer.shadowRadius = 20
        self.layer.shadowOpacity = 0.2
        self.backgroundColor = .white
        let shadowAmount: CGFloat = 2
        let rect = CGRect(x: 0, y: 2, width: self.bounds.width + shadowAmount * 0.1, height: self.bounds.height + shadowAmount * 0.1)
        self.layer.shadowPath = UIBezierPath(rect: rect).cgPath
    }
    
    func createImage(systemName: String) {
        guard let image = UIImage(systemName: systemName)?.withTintColor(.blue, renderingMode: .alwaysOriginal) else { return }
        let renderer = UIGraphicsImageRenderer(bounds: self.frame)
        let renderedImage = renderer.image { (_) in
            image.draw(in: frame.insetBy(dx: 30, dy: 30))
        }
        self.image = renderedImage
    }
}

class TestCustomViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemYellow
        
        let size = CGSize(width: 100, height: 100)
        let frame = CGRect(origin: .zero, size: size)
        
        let infoView       = CustomView(frame: frame, corners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], systemName: "info.circle")
        let locationView   = CustomView(frame: frame, corners: [], systemName: "location")
        let twoDView       = CustomView(frame: frame, corners: [.layerMinXMaxYCorner, .layerMaxXMaxYCorner], systemName: "view.2d")
        let binocularsView = CustomView(frame: frame, corners: [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner], systemName: "binoculars.fill")

        // create 2 separator views
        let sep1 = UIView()
        sep1.backgroundColor = .blue
        let sep2 = UIView()
        sep2.backgroundColor = .blue
        
        let stackView = UIStackView(arrangedSubviews: [infoView, sep1, locationView, sep2, twoDView, binocularsView])

        // use .fill, not .fillEqually
        stackView.distribution = .fill
        
        stackView.axis = .vertical
        stackView.setCustomSpacing(20, after: twoDView)

        view.addSubview(stackView)
        
        // let's use auto-layout
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        // respect safe area
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // only need top and leading constraints for the stack view
            stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
            stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 100.0),
        
            // both separator views need height constraint of 1.0
            sep1.heightAnchor.constraint(equalToConstant: 1.0),
            sep2.heightAnchor.constraint(equalToConstant: 1.0),
        ])
        
        // set all images to the same 1:1 ratio size
        [infoView, locationView, twoDView, binocularsView].forEach { v in
            v.widthAnchor.constraint(equalToConstant: size.width).isActive = true
            v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
        }
        
    }
}

This is what I get with that code example:

enter image description here

DonMag
  • 69,424
  • 5
  • 50
  • 86