【iOS】リンク先に遷移可能なチェックボックスを作ってみる

完成品

f:id:assaulter:20181108224732g:plain

Webだと稀にある、テキストまでクリック対象のチェックボックスかつ、リンクだけ別のアクション(別ページを開くとか)をiOSで実装してみた。

作って思ったけど、普通にチェックボックスだけタップ可で良い気がするわ...

ロジック

samwize.com

こちらにあるように、UILabelの特定の文字が押された判定はできる。 また、UIButtonのラベルはUILabelなので、この論理で実装できる。

作り方

Xcode: 10.1

UITapGestureRecognizerを拡張

元ネタだとちょっと古いので変更する

extension UITapGestureRecognizer {

    func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
        // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: CGSize.zero)
        let textStorage = NSTextStorage(attributedString: label.attributedText!)

        // Configure layoutManager and textStorage
        layoutManager.addTextContainer(textContainer)
        textStorage.addLayoutManager(layoutManager)

        // Configure textContainer
        textContainer.lineFragmentPadding = 0.0
        textContainer.lineBreakMode = label.lineBreakMode
        textContainer.maximumNumberOfLines = label.numberOfLines
        let labelSize = label.bounds.size
        textContainer.size = labelSize

        // Find the tapped character location and compare it to the specified range
        let locationOfTouchInLabel = self.location(in: label)
        let textBoundingBox = layoutManager.usedRect(for: textContainer)

        let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
        let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y:                                               locationOfTouchInLabel.y - textContainerOffset.y);
        let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)

        return NSLocationInRange(indexOfCharacter, targetRange)
    }
}

UIButtonにイベント等を設定

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!
    private var clickableRange: NSRange!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        setUpButton()
    }

    @objc func onClickButton(_ button: UIButton) {
        button.isSelected = !button.isSelected
    }

    @objc func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
        if gestureRecognizer.didTapAttributedTextInLabel(label: button.titleLabel!, inRange: clickableRange) {
            // 便器上アラートを表示している
            showAlert()
        } else {
            onClickButton(button)
        }
    }

    private func setUpButton() {
        // create attributed string
        let str = "利用規約を読みなんたからんたら"
        clickableRange = NSString(string: str).range(of: "利用規約")
        let attr = NSMutableAttributedString(
            string: str,
            attributes: [
                .font: UIFont(name: "HiraginoSans-W3", size: 16)!,
                .foregroundColor: UIColor.black
            ]
        )
        attr.addAttribute(.foregroundColor, value: UIColor.green, range: clickableRange)

        // set attributed string
        button.setAttributedTitle(attr, for: .normal)

        // add checkbox images
        button.setImage(UIImage.init(named: "off"), for: .normal)
        button.setImage(UIImage.init(named: "on"), for: .selected)
        button.titleEdgeInsets.left = 6

        button.addTarget(self, action: #selector(onClickButton(_:)), for: .touchUpInside)

        // create gesture
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapGesture(_:)))
        button.titleLabel?.addGestureRecognizer(tapGesture)
        button.titleLabel?.isUserInteractionEnabled = true
    }

    private func showAlert() {
        let alert = UIAlertController(title: "リンクを", message: "開く", preferredStyle: .alert)
        let cancel = UIAlertAction(title: "閉じる", style: .cancel)
        alert.addAction(cancel)
        self.present(alert, animated: true)
    }
}