UX는 무엇이 결정 하는가 [2부]

dev_will_d·2023년 4월 26일
1
post-thumbnail

UX는 무엇이 결정 하는가 [1부]에서는 왜 일관된 UX가 중요한지에 대해서 이야기를 했다. 이번 글에서는 실재 디자인적 일관성을 개발로 어떻게 구현했는지 설명하겠다.

이 프로젝트에 사용된 라이브러리

  • RxSwift
  • RxCocoa
  • RxGesture
  • Kingfisher
  • Snapkit
  • Then
  • PanModal
  • Toast-Swift
  • Lottie-ios

글을 읽기전 참고 블로그

iOS 화면 판단

  • 사용


  • 구현부
class CommonModal : BaseViewController {
    let disposeBag = DisposeBag()
    
    let blurView = UIView().then {
        $0.backgroundColor = .black.withAlphaComponent(0.3)
    }
    
    let parentView = UIView().then {
        $0.backgroundColor = .clear
        $0.layer.cornerRadius = 16
    }
    
    let titleLabel = UILabel().then {
        $0.textAlignment = .center
        $0.numberOfLines = 1
    }
    
    let imageView = UIImageView()
    
    let messageLabel = UILabel().then {
        $0.textAlignment = .center
        $0.numberOfLines = 0
    }
    
    let underLineView = UIView().then {
        $0.heightAnchor.constraint(equalToConstant: 1).isActive = true
    }
    
    let nagativeButton = UILabel().then {
        $0.textAlignment = .center
    }
    
    var nagativeDelegate : (CommonModal) -> Void = { _ in }
    
    let positiveButton = UILabel().then {
        $0.textAlignment = .center
    }
    
    var positiveDelegate : (CommonModal) -> Void = { _ in }
    
    
    lazy var buttonStackView = UIStackView(arrangedSubviews: [nagativeButton, positiveButton]).then {
        $0.heightAnchor.constraint(equalToConstant: 44).isActive = true
        $0.distribution = .fillEqually
        $0.axis = .horizontal
    }
    
    let pillarLineView = UIView().then {
        $0.widthAnchor.constraint(equalToConstant: 1).isActive = true
    }
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        layout()
        bind()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    /*
     화연에 표시
     */
    func show() {
        /*
         현재 화면의 최상단에 보이는 ViewController가 자기 자신인지 판단.
         */
        if topMostViewController !== self {
            /*
             최상단 ViewController를 통해서 Modal 보여주기
             */
            self.view.backgroundColor = .clear
            self.modalTransitionStyle = .crossDissolve
            self.modalPresentationStyle = .overFullScreen
            topMostViewController?.present(self, animated: true)
        }
    }
    
    func bind() {
        nagativeButton.rx.tapGesture()
            .when(.recognized)
            .bind(onNext : { [weak self] _ in
                guard let self = self else { return }
                self.nagativeDelegate(self)
            })
            .disposed(by: disposeBag)
        
        positiveButton.rx.tapGesture()
            .when(.recognized)
            .bind(onNext : { [weak self] _ in
                guard let self = self else { return }
                self.positiveDelegate(self)
            })
            .disposed(by: disposeBag)
    }
    
    private func configure(
        title : String,
        message : String,
        
        image : UIImage?,
        imageUrl : String?,
        imgWidth : Int,
        imgHeight : Int,
        
        nagativeButtonStr : String,
        nagativeButtonDelegate : @escaping (CommonModal) -> Void,
        positiveButtonStr : String,
        positiveButtonDelegate : @escaping (CommonModal) -> Void,
        
        /*
         속성
         */
        titleColor : UIColor,
        messageColor : UIColor,
        nagativeButtonColor : UIColor,
        positiveButtonColor : UIColor,
        modalBackgroundColor : UIColor,
        lineColor : UIColor
    ) {
        self.titleLabel.text = title
        
        self.messageLabel.text = message
        
        /*
         image
         */
        
        imageView.snp.makeConstraints {
            $0.width.equalTo(imgWidth)
            $0.height.equalTo(imgHeight)
        }
        
        if let image = image {
            imageView.image = image
            imageView.snp.makeConstraints {
                $0.top.equalTo(titleLabel.snp.bottom).offset(16)
            }
        }
        
        if let imageUrl = imageUrl {
            imageView.kf.setImage(with: URL(string: imageUrl))
            imageView.snp.makeConstraints {
                $0.top.equalTo(titleLabel.snp.bottom).offset(16)
            }
        }
        
        /*
         setNavigationButton을 안한다면 숨겨서 positiveButton만 보이게 동작하자...
         */
        self.nagativeButton.isHidden = nagativeButtonStr.isEmpty
        self.pillarLineView.isHidden = nagativeButtonStr.isEmpty
        
        self.nagativeButton.text = nagativeButtonStr
        self.nagativeDelegate = nagativeButtonDelegate
        
        self.positiveButton.text = positiveButtonStr
        self.positiveDelegate = positiveButtonDelegate
        
        /*
         속성
        */
        self.titleLabel.textColor = titleColor
        self.messageLabel.textColor = messageColor
        self.nagativeButton.textColor = nagativeButtonColor
        self.positiveButton.textColor = positiveButtonColor
        self.parentView.backgroundColor = modalBackgroundColor
        self.underLineView.backgroundColor = lineColor
        self.pillarLineView.backgroundColor = lineColor
    }
    
    /*
     자신의 Design에 맞게 layout을 배치
     정확히는 설정을 했을때 그릇이 되는 View들에 대해서
     각 상황에 대응하는 layout을 배치하는 것이 핵심
     */
    func layout() {
        [
            blurView,
            parentView,
            titleLabel,
            imageView,
            messageLabel,
            underLineView,
            buttonStackView,
            pillarLineView,
        ].forEach {
            view.addSubview($0)
        }
        
        blurView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
        
        parentView.snp.makeConstraints {
            $0.width.equalTo(270)
            $0.center.equalToSuperview()
            $0.top.equalTo(titleLabel).offset(-16)
            $0.bottom.equalTo(buttonStackView)
        }
        
        
        titleLabel.snp.makeConstraints {
            $0.top.equalTo(parentView)
            $0.centerX.equalTo(parentView).inset(16)
        }
        
        imageView.snp.makeConstraints {
            $0.top.equalTo(titleLabel.snp.bottom)
            $0.centerX.equalTo(parentView)
        }
        
        messageLabel.snp.makeConstraints {
            $0.top.equalTo(imageView.snp.bottom).offset(16)
            $0.leading.trailing.equalTo(parentView).inset(16)
        }
        
        underLineView.snp.makeConstraints {
            $0.top.equalTo(messageLabel.snp.bottom).offset(16)
            $0.leading.trailing.equalTo(parentView)
        }
        
        buttonStackView.snp.makeConstraints {
            $0.top.equalTo(underLineView.snp.bottom)
            $0.leading.trailing.equalTo(parentView)
            $0.bottom.equalTo(parentView)
        }
        
        pillarLineView.snp.makeConstraints {
            $0.top.bottom.equalTo(buttonStackView)
            $0.centerX.equalTo(buttonStackView)
        }
    }
    
    class Builder {
        /*
         ...
         이후 필요한 속성에 대해서 지속적으로 추가하면 된다
         ex) Title, Message, button Font
         */
        
        /*
         Default 값 설정
         */
        private var title : String = ""
        private var message : String = ""
        
        private var image : UIImage? = nil
        private var imageUrl : String? = nil
        private var imgWidt : Int = 0
        private var imghight : Int = 0
        
        private var nagativeButtonStr : String = ""
        private var nagativeButtonDelegate : (CommonModal) -> Void = { _ in }
        private var positiveButtonStr : String = ""
        private var positiveButtonDelegate : (CommonModal) -> Void = { _ in }
        
        /*
         color 속성
         Default 값 설정
         */
        private var titleColor : UIColor = .black
        private var messageColor : UIColor = .black
        private var nagativeButtonColor : UIColor = .blue
        private var positiveButtonColor : UIColor = .red
        private var modalBackgroindColor : UIColor = .white
        private var lineColor : UIColor = .gray
        
        
        func setTitle(_ title : String) -> Self {
            self.title = title
            return self
        }
        
        func setMessage(_ message : String) -> Self {
            self.message = message
            return self
        }
        
        func setImage(_ image : UIImage?, width : Int, height : Int) -> Self {
            self.image = image
            self.imgWidt = width
            self.imghight = height
            return self
        }
        
        func setImageUrl(_ url : String?, width : Int, height : Int) -> Self {
            self.imageUrl = url
            self.imgWidt = width
            self.imghight = height
            return self
        }
        
        func setNagativeButton(
            _ label : String,
            _ delegate : @escaping (CommonModal) -> Void
        ) -> Self {
            self.nagativeButtonStr = label
            self.nagativeButtonDelegate = delegate
            return self
        }
        
        func setPositiveButton(
            _ label : String,
            _ delegate : @escaping (CommonModal) -> Void
        ) -> Self {
            self.positiveButtonStr = label
            self.positiveButtonDelegate = delegate
            return self
        }
        
        /*
         color 속성
         */
        
        func setTitleColor(_ color : UIColor) -> Self {
            self.titleColor = color
            return self
        }
        
        func messageColor(_ color : UIColor) -> Self {
            self.messageColor = color
            return self
        }
        
        func setNagativeButtonColor(_ color : UIColor) -> Self {
            self.nagativeButtonColor = color
            return self
        }
        
        func setPositiveButtonColor(_ color : UIColor) -> Self {
            self.positiveButtonColor = color
            return self
        }
        
        func setModalBackgroundColor(_ color : UIColor) -> Self {
            self.modalBackgroindColor = color
            return self
        }
        
        func setLineColor(_ color : UIColor) -> Self {
            self.lineColor = color
            return self
        }
        
        func build() -> CommonModal {
            return CommonModal().then {
                $0.configure(
                    title: title,
                    message: message,
                    image: image,
                    imageUrl: imageUrl,
                    imgWidth: imgWidt,
                    imgHeight: imghight,
                    nagativeButtonStr: nagativeButtonStr,
                    nagativeButtonDelegate: nagativeButtonDelegate,
                    positiveButtonStr: positiveButtonStr,
                    positiveButtonDelegate: positiveButtonDelegate,
                    titleColor: titleColor,
                    messageColor: messageColor,
                    nagativeButtonColor: nagativeButtonColor,
                    positiveButtonColor: positiveButtonColor,
                    modalBackgroundColor: modalBackgroindColor,
                    lineColor: lineColor
                )
            }
        }
    }
    
    
}
  • 설명

    Builder 클래스는 데이터 세팅 및 CommonModal의 최종 생산(build) 역할을 담당한다. 그리고 CommonModal는 이 데이터를 담는 그릇배치 및 표현(show)의 역할을 담당한다.
    [자세한 설명은 주석 참고]

BottomModal

  • 사용

  • 구현부
struct CommoBottomModalAction {
    let title : String
    let titleColor : UIColor
    let action : (CommonBottomModal) -> Void
}

class CommonBottomModal : BaseViewController {
    
    let disposeBag = DisposeBag()
    
    let blurView = UIView().then {
        $0.backgroundColor = .clear
    }
    
    var actions : [CommoBottomModalAction] = []
    
    let stackView = UIStackView().then {
        $0.spacing = 5
        $0.axis = .vertical
    }
    
    let cancleButton = UILabel().then {
        $0.heightAnchor.constraint(equalToConstant: 56).isActive = true
        $0.backgroundColor = .clear
        $0.clipsToBounds = true
        $0.layer.cornerRadius = 16
        $0.textAlignment = .center
    }
    
    func show() {
        
        /*
         최상단 ViewController가 자기 자신인지 판단
         */
        if topMostViewController !== self {
            topMostViewController?.presentPanModal(self)
        }
    }
    
    private func configure(
        _ actions : [CommoBottomModalAction],
        _ actionBackgroundColor : UIColor,
        _ cancelMessage : String,
        _ cancelBackgroundColor : UIColor
    ) {
        
        /*
         actionData Label로 변환
         실질 적인 UI/UX를 하는 실재로 변환하는 과정
         */
        let labels = actions
            .map { action in
                UILabel().then {
                    $0.text = action.title
                    $0.textColor = action.titleColor
                    
                    $0.heightAnchor.constraint(equalToConstant: 56).isActive = true
                    $0.backgroundColor = actionBackgroundColor
                    $0.clipsToBounds = true
                    $0.layer.cornerRadius = 16
                    $0.textAlignment = .center
                }
            }
        
        cancleButton.text = cancelMessage
        cancleButton.backgroundColor = cancelBackgroundColor
        
        /*
         stackView에 추가
         */
        labels.forEach {
            stackView.addArrangedSubview($0)
        }
        stackView.addArrangedSubview(cancleButton)
        
        
        [
            blurView,
            stackView
        ].forEach {
            view.addSubview($0)
        }
        
        blurView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
        
        /*
         actions의 아이템 개수에 따라 stackView의 크기를 동적으로 결정
         */
        stackView.snp.makeConstraints {
            $0.leading.trailing.equalToSuperview().inset(16)
            $0.bottom.equalToSuperview().inset(38)
            /*
             주요 코드
             */
            $0.top.equalTo(labels[0])
        }
        
        
        /*
         bind
         */
        blurView.rx.tapGesture()
            .when(.recognized)
            .bind(onNext : { [weak self] _ in
                self?.dismiss(animated: true)
            })
            .disposed(by: disposeBag)
        
        cancleButton.rx.tapGesture()
            .when(.recognized)
            .bind(onNext : { [weak self] _ in
                self?.dismiss(animated: true)
            })
            .disposed(by: disposeBag)
        
        /*
         commonActionDelegate에서 정의한 함수 호출
         */
        labels
            .enumerated()
            .forEach { index, label in
                label.rx.tapGesture()
                .when(.recognized)
                .bind(onNext : { [weak self] _ in
                    guard let self = self else { return }
                    self.dismiss(animated: true) {
                        actions[index].action(self)
                    }
                })
                .disposed(by: disposeBag)
        }
    }
    
        
    class Builder {
        /*
         필요한 Design에 맞게 속성 추가
         */
        private var actions : [CommoBottomModalAction] = []
        
        private var actionBackground : UIColor = .white
        
        private var cancelMessage : String = "취소"
        private var cancelBackgroundColor : UIColor = .gray
        
        
        func setActions(_ actions : [CommoBottomModalAction]) -> Self {
            self.actions = actions
            return self
        }
        
        func setActionBackgroundColor(_ color : UIColor) -> Self {
            self.actionBackground = color
            return self
        }
        
        func setCancelMessage(_ message : String) -> Self {
            self.cancelMessage = message
            return self
        }
        
        func cancelBackgroundColor(_ color : UIColor) -> Self {
            self.cancelBackgroundColor = color
            return self
        }
        
        
        /*
         설정을 모두 완료하고 이때 CommonBottomModal 생성
         */
        func build() -> CommonBottomModal {
            return CommonBottomModal().then {
                $0.configure(
                    actions,
                    actionBackground,
                    cancelMessage,
                    cancelBackgroundColor
                )
            }
        }
        
    }
}


extension CommonBottomModal : PanModalPresentable {
    
    var showDragIndicator: Bool {
        return false
    }
    
    var panScrollable: UIScrollView? {
        return nil
    }
    
    var shortFormHeight: PanModalHeight {
        return .maxHeightWithTopInset(0)
    }
    
    var longFormHeight: PanModalHeight {
        return .maxHeightWithTopInset(0)
    }
    var anchorModalToLongForm: Bool {
        return false
    }
}
  • 설명

    기본 원리는 CommonModal과 같다. 다른점이 있다면 BottomModal의 경우에 Action의 개수가 다양 할 수있다. 예제 에서는 2개의 액션만 보여지지만 5개가 될 수 도 있다. 이 액션에 대해서 각각의 동작을 정의해 줘야 했기에 CommoBottomModalAction를 구현했고 이를 통해서 각각의 액션을 정의해 Builder에게 전달하는 식으로 개발을 했다.
    [자세한 설명은 주석 참고]

CommonToast

  • 사용
  • 구현
class CommonToast : UIView {
    
    
    private let disposeBag = DisposeBag()
    
    private var onClickDelegate : (CommonToast) -> Void = { _ in }
    
    private let messageLabel = UILabel().then {
        $0.textAlignment = .center
        $0.numberOfLines = 0
    }
    
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        layout()
        bind()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func show() {
        let topMostViewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController?.topMostViewController
        topMostViewController?.view.showToast(self, duration: 1, position: .bottom)
    }
    
    private func bind() {
        /*
         ToastButton을 탭 했을때
         */
        self.rx.tapGesture()
            .when(.recognized)
            .bind(onNext : { [weak self] _ in
                guard let self = self else { return }
                self.onClickDelegate(self)
            })
            .disposed(by: disposeBag)
    }
    
    private func configure(
        message : String,
        messageColor : UIColor,
        backgroundColor : UIColor,
        onClickDelegate : @escaping (CommonToast) -> Void
    ) {
        self.messageLabel.text = message
        self.messageLabel.textColor = messageColor
        
        self.backgroundColor = backgroundColor
        
        self.onClickDelegate = onClickDelegate
    }
    
    
    private func layout() {
        [
            messageLabel
        ].forEach {
            addSubview($0)
        }
        
        messageLabel.snp.makeConstraints {
            $0.center.equalToSuperview()
        }
    }
    
    
    /*
     필요에 의해서 추가 속성 등록 및 사용
     */
    
    class Builder {
        private var message : String = ""
        private var messageColor : UIColor = .black
        private var backgroundColor : UIColor = .cyan
        private var heightSize : CGFloat = 56
        
        private(set) var onClickDelegate : (CommonToast) -> Void = { _ in }
        
        func setMessage(_ message : String) -> Self {
            self.message = message
            return self
        }
        
        func setMessageColor(_ color : UIColor) -> Self {
            self.messageColor = color
            return self
        }
        
        func setBackgroundColor(_ color : UIColor) -> Self {
            self.backgroundColor = color
            return self
        }
        
        func setHeightSize(_ height : CGFloat) -> Self {
            self.heightSize = height
            return self
        }
        
        func setOnClickDelegate(_ delegate : @escaping (CommonToast) -> Void) -> Self {
            self.onClickDelegate = delegate
            return self
        }
        
        func build() -> CommonToast {
            let rootViewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController
            return CommonToast(frame: .init(x: 0, y: 0, width: rootViewController?.view.frame.width ?? 0, height: heightSize)).then {
                $0.configure(
                    message: message,
                    messageColor: messageColor,
                    backgroundColor: backgroundColor,
                    onClickDelegate: onClickDelegate
                )
            }
        }
    }   
}
  • 설명

    원리는 CommoModal과 같다.

CommonLoadingView

  • 사용


  • 구현
/*
 Lottie를 통한 LoadingView
 */

class CommonLoadingView {
    
    
    var loadingView : LottieAnimationView? = LottieAnimationView(name: "lottie").then {
    
        $0.isHidden = true
        $0.loopMode = .loop
        $0.play()
    }
    
    
    var timer : Timer? = nil
    var timeRemaining = 0.8
    
    
    
    convenience init() {
        
        let superView = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController?.topMostViewController?.view
        self.init(superView: superView)
    }
    
    
    init(superView : UIView?) {
        superView?.addSubview(loadingView!)
        loadingView?.snp.makeConstraints {
            $0.center.equalToSuperview()
        }
    }
    
    
    func show() {
        /*
         touch 비활성
         */
        let window = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window
        window?.isUserInteractionEnabled = false
        
        /*
         타이머가 동작하기 전에는 보여줘서는 안되기 때문에
         loadingView를 숨긴다.
         */
        loadingView?.isHidden = true
        
        startTimer()
    }
    
    
    func dismiss() {
        hide()
        stopTimer()
        loadingView?.removeFromSuperview()
        loadingView = nil
    }
    
    private func hide() {
        /*
         touch 활성
         */
        let window = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window
        window?.isUserInteractionEnabled = true
        
        self.loadingView?.isHidden = true
    }
    
    
    private func startTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: timeRemaining, repeats: true) { [weak self] _ in
            guard let self = self else { return }

            self.timeRemaining -= 0.8

            /*
             0.8초가 지나면 호출
             */
            if self.timeRemaining == 0 {
                /*
                 loadingView zIndex 최고 레벨 부여
                 */
                self.loadingView?.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude)
                self.loadingView?.isHidden = false
            }

        }
    }
    
    
    
    private func stopTimer() {
        timer?.invalidate()
        timer = nil
    }
    
}
  • 설명

    LoadingView는 Lottie-ios를 사용해서 표현했다. 내가 어디서 본지는 기억이 안나지만 사용자가 화면에서 집중력을 잃는 시간이 0.8초라고 들은적이 있다. 나는 이 근거로 화면에 LoadingView를 표시(show)할때 0.8초 전에는 LoadingView를 표시하지 않고 0.8초가 지나면 LoadingView를 표시하도록 로직을 작성해 봤다.
    또한 어떤 작업이 진행될때 사용자가 추가 동작을 하면 못하도록 화면의 Touch또한 막아줬다.(이 화면터치는 0.8초와 상관없이 dismiss가 호출될때까지 이루어 진다.)
    그리고 어떤 특정 작업(ex) Network)이 완료되면 LoadingView를 화면에서 지운도 그리고 터치도 다시 활성화 한다.(dismiss) 이번 예제에서는 Network 상황은 구현하지 않았고 대신에 timer를 통해 동작이 어떻게 이뤄지는지 구현했다.
    [자세한 내용은 주석 참고]

CommonRetry

  • 사용

    업로드중..
  • 구현

BaseViewController

DetailViewController

class DetailViewController : BaseViewController {
    let disposeBag = DisposeBag()
    
    let commonRetry = UILabel().then {
        $0.text = "CommonRetry"
    }
    
    
    
    
    var loadingView : CommonLoadingView? = nil
    var timeRemaining = 2.0
    var timer : Timer? = nil
    
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        layout()
        bind()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
...
    /*
    BaseViewController retry 함수 오버라이드
    */
    override func retry() {
        super.retry()
        print("HELLO RETRY")
    }
... 
}

CommonRetryView

class CommonRetryView : UIView {
    static let EXIST = 1
    
    let disposeBag = DisposeBag()
    
    let titleLabel = UILabel().then {
        $0.numberOfLines = 0
    }
    
    let retryButton = UILabel().then {
        $0.textColor = .systemRed
        $0.numberOfLines = 0
    }
    
    override var intrinsicContentSize: CGSize {
        let titleSize = titleLabel.intrinsicContentSize
        let buttonSize = retryButton.intrinsicContentSize
        let width = min(titleSize.width, buttonSize.width)
        let height = titleSize.height + 16 + buttonSize.height
        return CGSize(width: width, height: height)
    }
    
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        layout()
        bind()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func show() {
        let topMostViewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController?.topMostViewController
        let alreadyAdded = topMostViewController?.view.subviews.contains(where: { $0.tag == CommonRetryView.EXIST }) ?? false
        guard !alreadyAdded else { return }
        
        topMostViewController?.view.addSubview(self)
        self.snp.makeConstraints {
            $0.center.equalToSuperview()
        }
    }
    
    func bind() {
        retryButton.rx.tapGesture()
            .when(.recognized)
            .bind(onNext : { [weak self] _ in
                let topMostViewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController?.depthViewController
                let baseViewController = topMostViewController as? BaseViewController
                
                /*
                 BaseViewController에서 Override한 retry함수 호출
                 */
                baseViewController?.retry()
                
                self?.removeFromSuperview()
            })
            .disposed(by: disposeBag)
    }
    
    private func configure(
        _ title : String,
        _ buttonText : String
    ) {
        titleLabel.text = title
        retryButton.text = buttonText
        
        
        /*
         tag 설정
         */
        self.tag = CommonRetryView.EXIST
        
        /*
         frame 설정
         */
        self.frame = CGRect(origin: .zero, size: intrinsicContentSize)
    }
    
    
    func layout() {
        [
            titleLabel,
            retryButton
        ].forEach {
            addSubview($0)
        }
        
        titleLabel.snp.makeConstraints {
            $0.top.equalToSuperview()
            $0.centerX.equalToSuperview()
        }
        
        retryButton.snp.makeConstraints {
            $0.top.equalTo(titleLabel.snp.bottom).offset(16)
            $0.centerX.equalTo(titleLabel)
        }
        
    }
    
    class Builder {
        private var title : String    = ""
        private var retryStr : String = ""
        
        func setTitle(_ title : String) -> Self {
            self.title = title
            return self
        }
        
        func setRetryStr(_ retryStr : String) -> Self {
            self.retryStr = retryStr
            return self
        }
        
        func build() -> CommonRetryView {
            CommonRetryView().then {
                $0.configure(
                    title,
                    retryStr
                )
            }
        }
    }   
}
  • 설명

    CommonRetryView에서 재시도 버튼을 누르는 코드를 보면 현재 화면에 보이는 가장 자녀 ViewController를 호출한다. 이 ViewController는 BaseViewController를 상속 받은 ViewController일 것이다. 왜냐 하면 내가 그렇게 설계를 했으니까! 그리고 이 ViewController의 retry를 실행한다. 즉 retry 함수를 override를 하여 각 화면에 맞는 retry 로직을 작성하면 된다. 이렇게 하면 재시도에 대한 원하는 동작을 구현 할 수 있다!!

마무리

위와 같이 개발했을때 디자인의 일관성을 구현했는가? 구현했다고 할 수 있다. 이유는 내가 만약 Loading을 표현해야 되고 Modal을 표현 한다고 하면 위의 Component들을 사용할것이기 때문이다. 그리고 이 Component들은 각자의 일관된 설계를 가지고 있기때문에 이렇게 해서 나는 일관된 UI/UX를 구현했다. 한단계 더 나아가 나는 Builder에 속성을 추가만 하면 새롭게 속성을 설정할 수 있도록 확장성 있는코드도 작성해 봤다. 전체코드

네이티브 개발자는 최전선에서 사용자와 상호작용 하는 개발자이다.
개발자로서 여러 덕목도 있겠지만 좋은 UI/UX를 어떻게 사용자에게 전달해야 되는가 또한 매우 중요한 덕목이겠다. 그런 고민이 있기에 더 좋은 개발이 나올것이라고 믿는다. 앞으로도 나는 사용자에게 좋은 UI/UX를 제공하기 위해서는 어떻게 해야 되는가에대해서 고민하고 개발적으로 풀어나가는 노력을 열심히 할것이다.

profile
질문의 질이 답의 질을 결정한다.

0개의 댓글