관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] BaseViewController (Reachability, toast, dialog, loading - JGProgressHUD, setting, back pressed) 본문

iOS 응용 (swift)

[iOS - swift] BaseViewController (Reachability, toast, dialog, loading - JGProgressHUD, setting, back pressed)

jake-kim 2020. 12. 19. 16:53

* 한 프로젝트에 프레임워크를 추가하여 구분하기: ios-development.tistory.com/217

Toast View 

  • UIView를 interface builder로 초기화 할 때 사용될 함수를 common extension에 정의
// CommonExtension/Common/UIView
public extension UIView {

    func xibSetup() {
        guard let view = loadViewFromNib(nib: type(of: self).className) else {
            return
        }
        view.translatesAutoresizingMaskIntoConstraints = false
        view.frame = bounds
        addSubview(view)
        view.fillToSuperview(withPadding: .zero)
    }

    func fillToSuperview(withPadding padding: UIEdgeInsets) {
        anchor(top: superview?.topAnchor, leading: superview?.leadingAnchor, bottom: superview?.bottomAnchor, trailing: superview?.trailingAnchor, padding: padding)
    }

    func loadViewFromNib(nib: String) -> UIView? {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: nib, bundle: bundle)
        return nib.instantiate(withOwner: self, options: nil).first as? UIView
    }

    func anchor(top: NSLayoutYAxisAnchor? = nil, leading: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, trailing: NSLayoutXAxisAnchor? = nil, padding: UIEdgeInsets = .zero, size: CGSize = .zero, centerX: NSLayoutXAxisAnchor? = nil, centerY: NSLayoutYAxisAnchor? = nil) {
        translatesAutoresizingMaskIntoConstraints = false

        if let top = top {
            topAnchor.constraint(equalTo: top, constant: padding.top).isActive = true
        }

        if let leading = leading {
            leadingAnchor.constraint(equalTo: leading, constant: padding.left).isActive = true
        }

        if let bottom = bottom {
            bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom).isActive = true
        }

        if let trailing = trailing {
            trailingAnchor.constraint(equalTo: trailing, constant: -padding.right).isActive = true
        }

        if let centerX = centerX {
            centerXAnchor.constraint(equalTo: centerX).isActive = true
        }

        if let centerY = centerY {
            centerYAnchor.constraint(equalTo: centerY).isActive = true
        }

        if size.width != 0 {
            widthAnchor.constraint(equalToConstant: size.width).isActive = true
        }

        if size.height != 0 {
            heightAnchor.constraint(equalToConstant: size.height).isActive = true
        }
    }
}
// CommonExtension/Common/NSObject
public extension NSObject {
    var className: String {
        return String(describing: type(of: self))
    }

    class var className: String {
        return String(describing: self)
    }
}

width, height설정

  • ToastMessageView.swift 생성
import Foundation
import UIKit
import CommonExtension

class ToastMessageView: UIView {

    @IBOutlet weak var lblToastMessage: UILabel!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        xibSetup()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        xibSetup()
    }

    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        xibSetup()
    }
}
  • toast view로 띄울 수 있는 프레임워크 다운 (Toast_Swift)
pod 'Toast-Swift'
  • BaseViewController
import UIKit
import Toast_Swift

class BaseViewController: UIViewController {

    // init
    override func viewDidLoad() {
        super.viewDidLoad()
    }

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

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

    // toast
    func showToastView(message: String, point: CGPoint? = nil) {
        let messageView = ToastMessageView(frame: CGRect(x: 0, y: 0, width: 248, height: 48))
        messageView.layer.cornerRadius = messageView.bounds.height / 2
        messageView.clipsToBounds = true
        messageView.lblToastMessage.text = message

        if let point = point {
            view.showToast(messageView, point: point)
        } else {
            view.showToast(messageView)
        }
    }
}
  • 테스트)
class ViewController: BaseViewController {
    
    @IBAction func showToast(_ sender: Any) {
        showToastView(message: "jake블로그(토스트 메세지)")
    }

}


Loading

  • cocoapods
pod 'JGProgressHUD'
  • 보조적으로 사용될 Rx
  pod 'RxSwift'
  pod 'RxCocoa'
  • 로딩 객체 hud  생성
// BaseViewController

	lazy var hud: JGProgressHUD = {
        let loader = JGProgressHUD(style: .dark)
        return loader
    }()
  • loading함수 추가
    // Loading
    func showLoading() {
        DispatchQueue.main.async {
            self.hud.show(in: self.view, animated: true)
        }
    }
    func hideLoading() {
        DispatchQueue.main.async {
            self.hud.dismiss(animated: true)
        }
    }
  • Rx바인딩을 위한 Reactive extension
extension Reactive where Base: BaseViewController {
    var showLoading: Binder<Void> {
        return Binder(self.base) { (vc, show) in
            vc.showLoading()
        }
    }
    var hideLoading: Binder<Void> {
        return Binder(self.base) { (vc, show) in
            vc.hideLoading()
        }
    }
}
  • 테스트)
// ViewController
	@IBAction func showLoading(_ sender: Any) {
        showLoading()
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            self.hideLoading()
        }
    }

// Reactive extension 테스트
    @IBOutlet weak var btnLoading: UIButton!
    let bag = DisposeBag()

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        btnLoading.rx.tap.asDriver(onErrorRecover: {_ in .never()})
            .drive(rx.showLoading)
            .disposed(by: bag)
    }

 


Setting화면으로 이동

  • alert띄우는 showAlert함수를 extension 추가
import UIKit

// CommonExtension/Common/UIViewController
public extension UIViewController {
    func showAlert(title: String? = "", message: String?, actionTitle: String = "OK", actionCallback: (() -> Void)? = nil) {
        let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alertController.addAction(UIAlertAction(title: actionTitle, style: .default, handler: { (_) in
            actionCallback?()
        }))
        present(alertController, animated: true, completion: nil)
    }
}
  • 알림 및 설정 화면으로 이동
    // Alert and open setting
    func showAlertAndSetting(alertTitle: String, actionTitle: String) {
        showAlert(title: alertTitle, message: nil, actionTitle: actionTitle) { [weak self] in
            self?.openSettingsInApp()
        }
    }
    private func openSettingsInApp() {
        if let url = URL(string: UIApplication.openSettingsURLString) {
            if UIApplication.shared.canOpenURL(url) {
                UIApplication.shared.open(url, completionHandler: nil)
            }
        }
    }
  • 테스트)
    @IBAction func openSetting(_ sender: Any) {
        showAlertAndSetting(alertTitle: "alert타이틀", actionTitle: "action타이틀(설정으로 이동)")
    }

back pressed (화면전환)

  • 이 함수를 호출하면 NavigationController로 push한 경우, present한 경우 모두, 현재 노출된 화면을 제거하도록 하는 함수
extension Reactive where Base: BaseViewController {
    var backPressed: Binder<Void> {
        return Binder(base) { vc, _ in
            vc.view.endEditing(true)
            if vc.navigationController != nil {
                vc.navigationController?.popViewController(animated: true)
            } else {
                vc.dismiss(animated: true)
            }
        }
    }
}

공통 dialog

 

 

    var showServerErrorDialog: Binder<String> {
        return Binder(base) { (vc, errorMsg) in
            debugPrint(errorMsg)
            let dialogVC = DialogBuilder.serverErrorDialog()
            vc.present(dialogVC, animated: true)
        }
    }
  • 테스트)
// ViewController
		btnDialog.rx.tap.asDriver(onErrorRecover: { _ in .never()})
            .map { "server error in viewWillAppear" }
            .drive(rx.showServerErrorDialog)
            .disposed(by: bag)

network 상태 체크 - Reachability

  • cocoapods
pod 'RxReachability'
  • 네트워크 상태 바인딩 할 변수와 바인딩 함수 추가
// BaseViewController

var reachability: Reachability? // 이 변수를 바인딩 하여 네트워크 상태 점검
    let isConnected: PublishSubject<Bool> = .init() // 사용하는 입장에서 network 상태를 보고 사용할 용도의 변수
    var networkListener: NetworkListener = .off { // 네트워크 체크가 필요할 시 VC에서 netwrokListener = .on으로 설정
        didSet {
            if networkListener  == .on {
                setupReachabilityBindings()
            }
        }
    }
    
    
    private func setupReachabilityBindings() {
    
    	// 변화 감지 - 메세지 출력
        reachability?.rx.reachabilityChanged
            .subscribe(onNext: { reachability in
                print("reachability: changed => \(reachability.connection)")
            })
            .disposed(by: bag)

		// 네트워크가 꺼질 때 에러 dialog띄우는 부분
        reachability?.rx.isReachable
            .filter { !$0 }
            .map { _ in "server Error" }
            .asDriver(onErrorRecover: { _ in .never()})
            .drive(rx.showServerErrorDialog)
            .disposed(by: bag)

		// 네트워크 연결한 경우, isConnected에 true로 저장
        reachability?.rx.isConnected
            .map { _ in true }
            .bind(to: isConnected)
            .disposed(by: bag)

		// 네트워크 끊긴 경우, isConnected에 false로 저장
        reachability?.rx.isDisconnected
            .map { _ in false }
            .bind(to: isConnected)
            .disposed(by: bag)
    }
  • reachability변수 초기화
// BaseViewController

    override func viewDidLoad() {
        super.viewDidLoad()
        reachability = Reachability()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        try? reachability?.startNotifier()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        reachability?.stopNotifier()
    }
  • 테스트)
// ViewController

    override func viewDidLoad() {
        super.viewDidLoad()
        networkListener = .on
    }

* 전체 소스코드: github.com/JK0369/BaseViewController

Comments