Swift에서 동적 입사 UI 만들기

29715 단어 swiftios
새로운 기능을 만들 때 사용자가 그것을 어떻게 사용할지 고려하는 것이 매우 중요하다.대부분의 경우 사용자 인터페이스는 매우 간단하다.그러나, 때때로, 당신은 단추나 스위치를 강조하고 정보를 첨부하는 지도를 제공하려고 할 때가 있다.오늘 Swift에 재사용 가능하고 적응성이 강한 커버를 만들어서 차량을 싣고 이동하는 사용자가 당신의 모든 기능을 실현할 수 있도록 돕겠습니다.

요구 사항


입사 여행에서 시작했다면 기본적인 UX 모범 사례를 이해하는 것이 좋은 시작이다.
본질적으로 우리는 최상의 사용자 체험을 제공하고 사용자를 방해하지 않는 상황에서 사용자에게 충분한 정보를 제공하기를 바란다.이 방면에서 일부 방향을 제공하는 방법은 현재 발생하고 있는 일을 더 잘 이해하기 위해 도구 알림을 표시하는 것이다.본문에서, 나는 이 간단한 보기를 위해 하나를 만들 것이다.

비밀 번호


나중에 다시 사용할 수 있는 UI 구성 요소를 만들고 싶기 때문에 이미지나 어떤 방식으로도 정적 내용을 처리할 수 없습니다.만약 우리의 단추 복사본이나 배경색이 변한다면, 그것은 여전히 정확하게 표시해야 한다.
우선, 이 새 구성 요소를 위한 보기를 만듭니다.이것은 닻이 있어야 한다. 지원하는 구성 요소 (여기는 단추), 겹쳐진 배경 보기, 그리고 제목이 있어야 한다. 이것은 단추 위의 '풍선' 으로 표시될 것이다.
덮어쓰기 그림을 표시하는 데 다른 두 가지 기능이 필요합니다. 하나는 덮어쓰기 그림을 숨기는 데 사용됩니다.사용자가 언제 상호작용을 하는지 알 수 있도록 클릭 제스처도 추가할 것입니다.
class OverlayView: UIView {

    let title: String
    weak var anchorView: UIView?
    var onTap: (() -> Void)?

    init(title: String, anchorView: UIView?) {
        self.title = title
        super.init(frame: .zero)
        self.anchorView = anchorView
        setupViews()
    }

    required init?(coder: NSCoder) {
        fatalError("Not implemented")
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        setupViews()
    }

    func setupViews() {
        alpha = 0

        // TODO
    }

    func showOverlay() {
        UIView.animate(withDuration: 0.6) {
            self.alpha = 1
        }
    }

    func hideOverlay(_ completion: ((Bool) -> Void)? = nil) {
        UIView.animate(withDuration: 0.6, animations: {
            self.alpha = 0
        }, completion: completion)
    }
}
좋습니다. 우리는 이미 몇 가지 기본 애니메이션의 기초를 가지고 있지만, 등등, 그것은 어떠한 UI도 포함하지 않습니다.전시 닻부터 시작합시다.
그렇다면, 우리는 어떻게 해야만 커버층에 앵커 보기를 표시할 수 있습니까?
우리는 그것을 하위 보기로만 추가할 수 없고, 동작도 가져다 줄 수 있다. 이상적인 상황에서 우리는 첫 번째 디자인을 어지럽히고 싶지 않기 때문에 그것에 대한 강한 인용을 유지할 수 없다.
또한 개체 자체를 "복제"할 수 없습니다. UIView가 NSObject에서 상속된 경우에는 복제가 불가능하고 애플리케이션이 충돌할 수 있습니다.UIView를 다시 만들고 각 속성을 재할당하는 것이 가장 좋은 선택이지만, 이것은 확장된 작업에서 다시 사용하기 어려운 뷰 유형을 알아야 한다는 것을 의미합니다.
그러나 등등, 우리는 실제적으로 대상의 사본이 필요 없고 표시된 사본만 있으면 된다. 다행히도 UIView를 통해 실현할 수 있는 방법UIView.snapshotView(afterScreenUpdates: Bool)을 사용하여 스냅숏을 찍을 수 있다.바로 우리가 필요로 하는 것이다.
또한 컨테이너 뷰, 앵커 점을 가리키는 화살표 및 무시된 배경 뷰를 풍선에 추가합니다.나는 불활성 구성 요소와 레이아웃 제약을 사용하여 모든 구성 요소를 정렬할 것이다.
class OverlayView: UIView {
    // ... previous code

    lazy var titleLabel: UILabel = {
        let titleLabel = UILabel()
        titleLabel.font = UIFont.systemFont(ofSize: 12)
        titleLabel.text = title
        titleLabel.textColor = .label
        titleLabel.isHidden = title.isEmpty
        titleLabel.numberOfLines = 0
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        return titleLabel
    }()

    lazy var containerView: UIView = {
        let view = UIView()
        view.backgroundColor = .white
        view.layer.cornerRadius = 12
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    lazy var backgroundView: UIView = {
        let view = UIView()
        view.backgroundColor = .black
        view.alpha = 0.5
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    lazy var arrowImage: UIImageView = {
        let imageView = UIImageView(image: UIImage(named: "bottom-arrow"))
        imageView.contentMode = .scaleAspectFit
        imageView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            imageView.widthAnchor.constraint(equalToConstant: 24),
            imageView.heightAnchor.constraint(equalToConstant: 8),
        ])
        return imageView
    }()


    private func setupViews() {
        alpha = 0

        guard let anchorView = self.anchorView,
              let cloneView  = anchorView.snapshotView(afterScreenUpdates: false) else { return }

        cloneView.frame = anchorView.frame
        containerView.addSubview(titleLabel)

        addSubview(backgroundView)
        addSubview(cloneView)
        addSubview(arrowImage)
        addSubview(containerView)

        var constraints = [
            backgroundView.topAnchor.constraint(equalTo: topAnchor),
            backgroundView.leftAnchor.constraint(equalTo: leftAnchor),
            backgroundView.rightAnchor.constraint(equalTo: rightAnchor),
            backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
            titleLabel.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 12),
            titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12),
            titleLabel.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: -12),
            titleLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -12),
            containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 12),
            containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: -12),
            arrowImage.centerXAnchor.constraint(equalTo: cloneView.centerXAnchor),
            containerView.bottomAnchor.constraint(equalTo: cloneView.topAnchor, constant: -20),
            arrowImage.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -1)
        ]

        NSLayoutConstraint.activate(constraints)
    }
}
듣기에 괜찮다!그러나 시동을 걸 때 실제로는 작동하지 않는다.그것은 최초의 버튼에서 매우 멀다.

문제는 우리 아나운서의 캡처를 찍을 때 크기와 출처를 얻었다는 것이다.그러나 프레임은 부모 뷰를 기반으로 하므로 원점을 덮어쓰기로 변환하여 항상 작업해야 합니다.
private func setupViews() {
    alpha = 0

    guard let anchorView = self.anchorView,
            let parentView = anchorView.superview,
            let cloneView  = anchorView.snapshotView(afterScreenUpdates: false) else { return }

    let translatedOrigin = parentView.convert(anchorView.frame.origin, to: self)
    cloneView.frame = CGRect(origin: translatedOrigin, size: anchorView.bounds.size)

    // ... previous code
}

많이 좋아졌어요.
마지막으로, 우리는 부족한 클릭 제스처를 추가하고 포장하여 실현할 수 있으며, 이렇게 하면 어떠한 상호작용도 결국 그것을 무시할 수 있다.
private func setupViews() {
    // ... previous code

    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTapGesture(_:)))
    addGestureRecognizer(tapGestureRecognizer)
}

@objc func onTapGesture(_ sender: Any) {
    onTap?()
}
자, 마지막으로 ViewController의 실현을 확대해 봅시다.
class ViewController: UIViewController {
    // ... other UI elements


    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        showFirstOverlay()
    }

    func showFirstOverlay() {
        let overlayView = OverlayView(title: "This is your last chance. After this there is no turning back. You take the blue pill, the story ends; you wake up in your bed and believe whatever you want to believe.", anchorView: blueButton)
        overlayView.frame = view.frame
        view.addSubview(overlayView)

        overlayView.onTap = { [weak self, weak overlayView] in
            overlayView?.hideOverlay { _ in
                overlayView?.removeFromSuperview()
                self?.showSecondOverlay()
            }
        }
        overlayView.showOverlay()
    }

    func showSecondOverlay() {
        let overlayView = OverlayView(title: "You take the red pill, you stay in Wonderland and I show you how deep the rabbit hole goes.", anchorView: redButton)
        overlayView.frame = view.frame
        view.addSubview(overlayView)

        overlayView.onTap = { [weak overlayView] in
            overlayView?.hideOverlay { _ in
                overlayView?.removeFromSuperview()
            }
        }
        overlayView.showOverlay()
    }
}
덮어쓰기를 프로그래밍 방식으로 추가했기 때문에, 프레임 매칭을 발표자의 프레임 매칭으로 설정했기 때문에 전체 화면을 차지합니다.또한 viewDidAppear를 사용하여 이전 UIView 스냅샷이 작동하지 않도록 먼저 모든 컨텐트를 렌더링합니다.
마지막으로, 나는 링크를 덮어쓰고, 먼저 파란색 단추를 표시한 다음, 사용자와 상호작용한 후에 빨간색 단추를 표시할 것이다.앞으로 나아가기 전에 커버 레이어를 제거해야 합니다.
이곳은 완공된 후의 모습이다.

대단합니다. 우리의 커버층은 반복적으로 사용하기 쉽고, 곧 출시될 새로운 기능을 위해 매끄러운 설치를 만들 수 있습니다.우리가 더 잘할 수 있을까?

개선 및 제한 사항


그것을 더욱 중용성 있게 하기 위해서 나는 몇 가지 개선을 생각해 낼 수 있다.
현재 풍선 도움말의 방향은 항상 앵커 위에 있으며 위쪽에 가까이 있으면 작동하지 않습니다.이상적인 상황에서 우리는 커버층이 그 환경에 적응하기를 희망한다. 만약에 프레임 위에 공간이 없으면 최종적으로 닻 아래에 나타난다.
과도와 시간 애니메이션을 더 잘 제어할 수 있도록 타이머 설정도 설정할 수 있습니다. 현재는 하드코딩입니다.시간도 가장 좋은 것은 아니다. 우리는 사용자가 상호작용을 시작한 후에 다시 보여줄 수 있다.
또한 ViewController가 이 흐름을 가지고 있기 때문에 덮어쓰기 보기를 삭제할 수 없거나 잘못 쌓기 시작합니다.폐쇄에 의존하기 때문에 약한 인용과 강한 인용에 주목하는 것도 중요하다.
마지막으로 다른 하위 보기가 덮어쓰기를 표시하면 작동하지 않고 프레임은 자신의 용기 보기만 제한됩니다.따라서 여기서 작동하는 경우 하위 뷰가 너무 많으면 옵션이 제한될 수 있습니다.
한 마디로 하면, 우리는 다시 사용할 수 있는 동적 구성 요소를 만들기 위해 간단한 덮어쓰기 보기를 만드는 방법을 알고 있으며, 입사 여행에 매우 적합하다.
그것의 이산성은 모바일 사용자들이 자신의 방식을 찾을 수 있도록 도와주기에 충분하며, 지나치게 서둘러 다음 단계를 추진할 필요가 없다.이것이 바로 휴대전화 사용자로 하여금 좋은 체험을 하게 하는 원인이다.
이 예는 아래 GithubOverlaySample 프로젝트에서 사용할 수 있습니다.
언제든지 저를 팔로우하고 더 많은 정보를 얻으세요.

좋은 웹페이지 즐겨찾기