iOS BottomTap 메모리 및 라이프사이클 최적화 과정

dev_will_d·2023년 4월 18일
1
post-thumbnail
post-custom-banner

요구사항 및 상황

  1. 초기에 보여지는 화면은 Swift 화면으로 지정한다.
  2. Kotlin, Swift, CSharp은 BottomTap으로 화면전환
  3. Meta, Java는 화면전환을 일어나지 않게 하면서 새로운
    ViewController를 Present
//
//  MainViewController.swift
//  BottomTapMemoryAndLifeCycleOptimization
//
//  Created by 도학태 on 2023/04/18.
//

import Foundation
import UIKit
import RxSwift
import RxCocoa
import PanModal


class MainTapBarViewController : UITabBarController {
    static let KOTLIN = 0
    static let SWIFT  = 1
    static let JAVA   = 2
    static let META   = 3
    static let CSHARP = 4
    
    
    var isSelectConrifm = true
    var translatePageLastIndex = -1
    
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        attribute()
        bind()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        launchScreen()
    }
    
    func bind() {
        
        let emptyVc = UIViewController()
        
        let kotlinVc = KotlinViewController()
        let swiftVc = SwiftViewController()
        let cSharpVc = CSharpViewController()
        viewControllers = [
            generateNavController(kotlinVc, "Kotil", UIImage(systemName: "circle.square")),
            generateNavController(swiftVc, "Swift", UIImage(systemName: "flag.2.crossed.circle.fill")),
            generateNavController(emptyVc, "Meta", UIImage(systemName: "person.fill")),
            generateNavController(emptyVc, "Java", UIImage(systemName: "train.side.front.car")),
            generateNavController(cSharpVc, "CSharp", UIImage(systemName: "lock.display")),
        ]
    }
    
    /*
     초기 화면을 정한다.
     UITabbarViewController가 viewDidLoad에서 초기화가 완료된 이후에
     lanchScreen 호출 -> viewWillAppear에서 호출
     */
    
    func launchScreen() {
        if isSelectConrifm {
            /*
             초기 화면 SWIFT 설정
             */
            self.selectedIndex = MainTapBarViewController.SWIFT
            isSelectConrifm.toggle()
            
            self.translatePageLastIndex = MainTapBarViewController.SWIFT
        }
        
    }
    
    /*
     TabBar에 ViewController 등록하는 함수
     */
    func generateNavController(
        _ vc : UIViewController,
        _ title : String,
        _ image : UIImage?
    ) -> UINavigationController {
        vc.view.backgroundColor = .white
        let navController = UINavigationController(rootViewController: vc)
        let tabBarItem = UITabBarItem(title: title, image: image, tag: 0)
        navController.tabBarItem = tabBarItem
        
        return navController
    }
    
    func attribute() {
        self.delegate = self
    }
}

extension MainTapBarViewController : UITabBarControllerDelegate {
    
    /*
     Page 전환 로직이 담겨 있는 함수
     Bottom Tap 할때 이 함수가 호출
     */
    func translatePage(_ selectIndex : Int) {
        
        /*
         정상적으로 페이지 전환해야 되는 Page에는 LastIndex를 넣어준다
         Page전환이 되면 안되는 페이지는 LastIndex를 넣어주지 않고 LastIndex에 있는 값으로 페이지 유지하고
         새로운 페이지를 띄운다.
         */
        switch selectIndex {
        case MainTapBarViewController.META:
            self.selectedIndex = translatePageLastIndex
            
            let metaVc = MetaViewController()
            metaVc.view.backgroundColor = .white
            
            let navVc = UINavigationController(rootViewController: metaVc)
            navVc.modalTransitionStyle = .coverVertical
            navVc.modalPresentationStyle = .fullScreen
           
            self.present(navVc, animated: true)
             
        case MainTapBarViewController.JAVA:
            self.selectedIndex = translatePageLastIndex

            let javaVc = JavaViewController()
            javaVc.view.backgroundColor = .white
            
            self.presentPanModal(javaVc)
        default:
            self.translatePageLastIndex = selectIndex
        }
    }
    
    func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
        
        translatePage(self.selectedIndex)
        
    }
}

위와 같은 방식으로 했을때 문제점

  1. TabBarViewController에 ViewController를 등록할때 화면에 배치될 ViewController를 미리 생성하고 등록을 했다.
    이렇게 하면 화면은 생성되어 있는데 사용자가 그 화면을 보지 않는다고 하면 메모리 측면에서 비효율성을 야기한다.

  1. 나는 위에서 translatePageLastIndex와 selectedIndex를 활용하여 페이지가 전환되지 않게 했다 (정확히는 페이지 전환이 안되는것처럼 처리 한것이다.) 이렇게 했을때 문제는 LifeCycle에 문제가 생긴다.
  • Swift Page를 보고있을때 MetaViewController를 Present하면 아래의 로그와 같이 Swift Page가 다시 ViewDisAppear, ViewWillAppear이 호출이 되는것을 볼 수 있다.

    이러한 이유는 self.selectedIndex에 tanslatePageLastIndex를 넣어주고 화면을 띄어서 발생하는 문제이다.


판단

  • 위와 같은 방식으로 했을때 요구사항은 충족한다. 그러나 효율적이지 못하고 LifeCycle이 내가 원하지 않는 동작을 한다는것은
    적합하고 생각하지 않았다.

  • 어떻게 하면 이러한 문제를 해결 할 수 있을까?
    첫번째 문제의 경우 미리 등록을 해서 문제였다. 그렇다면 사용자가 탭을 했을때 생성을 하도록 시점을 달리하면 되지 않을까?

    두번째 문제는 selectedIndex에 LastIndex를 부여하고 새로운 페이지를 띄어서 문제가 되는것이다. 즉 페이지 전환이 되고
    또 페이지 전환이 되어 안되는것 처럼 보이는 것이다. 그렇다면 애초에 페이지 전환을 안하도록 막으면 되지 않을까?

해결

  • 첫번째 문제의 해결은 초기에 빈 ViewController를 넣어준다. 그리고 사용자가 버튼을 탭하는 시점에 맞게 ViewControlelr를 생성하고 다시
    화면에 등록시켜 줬다. 코드는 아래와 같다.


    *** 이와 같은 방법으로 했을때 아래와 같이 해결이 된다. 보는 바와 같이 SwiftPage를 보고 있다면 초기에 SwiftPage만 생성된다.
    또한 전환을 할때 그에 맞게 생성이 된다.

  • 두번째 문제 해결 : UITabBarControllerDelegate에서 제공해주는 아래의 함수는 페이지 전환에 대해서 어떻게 할지 정하는 함수인데
    아래의 함수를 통해서 문제를 해결했다 해결한 코드는 아래와 같다.

func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool 


*** 이와 같은 방법으로 했을때 아래와 같이 해결이 된다. 보는바와 같이 다시 viewWillAppear이 호출되지 않는다. 라이프 사이클 관리를
내가 원하는 동작대로 관리 할 수 있다.
업로드중..


  • 전체 코드
//
//  MainViewController.swift
//  BottomTapMemoryAndLifeCycleOptimization
//
//  Created by 도학태 on 2023/04/18.
//

import Foundation
import UIKit
import RxSwift
import RxCocoa
import PanModal


class MainTapBarViewController : UITabBarController {
    static let KOTLIN = 0
    static let SWIFT  = 1
    static let META   = 2
    static let JAVA   = 3
    static let CSHARP = 4
    
    
    var isSelectConrifm = true
    
    var mViewControllers : [String : UIViewController] = [:]
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        attribute()
        bind()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        launchScreen()
    }
    
    func bind() {
        
        let emptyVc = UIViewController()
        viewControllers = [
            generateNavController(emptyVc, "Kotil", UIImage(systemName: "circle.square")),
            generateNavController(emptyVc, "Swift", UIImage(systemName: "flag.2.crossed.circle.fill")),
            generateNavController(emptyVc, "Meta", UIImage(systemName: "person.fill")),
            generateNavController(emptyVc, "Java", UIImage(systemName: "train.side.front.car")),
            generateNavController(emptyVc, "CSharp", UIImage(systemName: "lock.display")),
        ]
    }
    
    /*
     초기 화면을 정한다.
     UITabbarViewController가 viewDidLoad에서 초기화가 완료된 이후에
     lanchScreen 호출 -> viewWillAppear에서 호출
     */
    
    func launchScreen() {
        if isSelectConrifm {
            /*
             초기 화면 SWIFT 설정
             */
            self.selectedIndex = MainTapBarViewController.SWIFT
            
            self.viewControllers?[MainTapBarViewController.SWIFT] = createViewController(MainTapBarViewController.SWIFT)
            isSelectConrifm.toggle()
        }
        
    }
    
    /*
     TabBar에 ViewController 등록하는 함수
     */
    func generateNavController(
        _ vc : UIViewController,
        _ title : String,
        _ image : UIImage?
    ) -> UINavigationController {
        vc.view.backgroundColor = .white
        let navController = UINavigationController(rootViewController: vc)
        let tabBarItem = UITabBarItem(title: title, image: image, tag: 0)
        navController.tabBarItem = tabBarItem
        
        return navController
    }
    
    func attribute() {
        self.delegate = self
    }
    
    
}

extension MainTapBarViewController : UITabBarControllerDelegate {
    
    /*
     ViewController 생성
     */
    func createViewController(_ index : Int) -> UIViewController {
        switch index {
        case MainTapBarViewController.KOTLIN:
            if mViewControllers["KOTLIN"] == nil {
                let vc = KotlinViewController()
                vc.view.backgroundColor = .white
                vc.tabBarItem = UITabBarItem(title: "Kotlin", image: UIImage(systemName: "circle.square"), selectedImage: nil)
                mViewControllers["KOTLIN"] = UINavigationController(rootViewController: vc)
            }
            return mViewControllers["KOTLIN"] ?? UIViewController()
        case MainTapBarViewController.SWIFT:
            if mViewControllers["SWIFT"] == nil {
                let vc = SwiftViewController()
                vc.view.backgroundColor = .white
                vc.tabBarItem = UITabBarItem(title: "Swift", image: UIImage(systemName: "flag.2.crossed.circle.fill"), selectedImage: nil)
                mViewControllers["SWIFT"] = UINavigationController(rootViewController: vc)
            }
            return mViewControllers["SWIFT"] ?? UIViewController()
        case MainTapBarViewController.CSHARP:
            if mViewControllers["CSHARP"] == nil {
                let vc = CSharpViewController()
                vc.view.backgroundColor = .white
                vc.tabBarItem = UITabBarItem(title: "CSharp", image: UIImage(systemName: "lock.display"), selectedImage: nil)
                mViewControllers["CSHARP"] = UINavigationController(rootViewController: vc)
            }
            return mViewControllers["CSHARP"] ?? UIViewController()
        default:
            return UIViewController()
        }
    }
    
    
    /*
     Page 전환 로직이 담겨 있는 함수
     Bottom Tap 할때 이 함수가 호출
     */
    func translatePage(_ selectIndex : Int) {
        
        /*
         정상적으로 페이지 전환해야 되는 Page에는 LastIndex를 넣어준다
         Page전환이 되면 안되는 페이지는 LastIndex를 넣어주지 않고 LastIndex에 있는 값으로 페이지 유지하고
         새로운 페이지를 띄운다.
         */
        switch selectIndex {
        case MainTapBarViewController.META:
            let metaVc = MetaViewController()
            metaVc.view.backgroundColor = .white
            
            let navVc = UINavigationController(rootViewController: metaVc)
            navVc.modalTransitionStyle = .coverVertical
            navVc.modalPresentationStyle = .fullScreen
           
            self.present(navVc, animated: true)
             
        case MainTapBarViewController.JAVA:
            let javaVc = JavaViewController()
            javaVc.view.backgroundColor = .white
            
            self.presentPanModal(javaVc)
        default:
            break
        }
    }
    
    
    
    /*
     Button Tap 관련 함수
     */
    func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
        /*
         META와 JAVA는 화면 전환을 막고 나머지는 화면전환을 해준다.
         또한 META와 JAVA에서는 화면 전환을 막기 전 전환 처리를 해줬다.
         */
        let index = tabBarController.viewControllers?.firstIndex(of: viewController)
        switch index {
        case MainTapBarViewController.META, MainTapBarViewController.JAVA:
            self.translatePage(index ?? 0)
            return false
        default:
            return true
        }
    }
    
    func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
        
        /*
         Kotlin, SWIFT, CSHARP는 버튼탭을 할때 emptyViewController에서 각각의 ViewController로 바꿔준다
         즉 Create함수에서 보다 싶이 특정 버튼을 탭했을때 생성한다.
         */
        if selectedIndex == MainTapBarViewController.KOTLIN || selectedIndex == MainTapBarViewController.SWIFT || selectedIndex == MainTapBarViewController.CSHARP {
            viewControllers?[selectedIndex] = createViewController(selectedIndex)
        }
    }
}

오늘은 BottoTap에서 어떻게 메모리를 효율적으로 관리할것이가, 라이프 사이클은 특정 상황에서 내가 원하는 동작으로 어떻게 동작하게 만들 수 있을까에 대해서 글을 작성해봤다. 내가 가장 중요하게 생각하는 부분은 판단 부분이다. 나에게 개발을 가르켜주신 우리 교수님께서는 나에게 항상
강조한 부분이 개발자는 코딩을 하는 사람이기 이전에 생각을 하는 사람이라고 하셨다. 결국 이러한 관점에서 봤을때 언어나 다른 모든 수단은
도구에 불가하다고.. 아직도 그 말씀을 기억하면서 개발을 하고 있다. 아직 부족하지만 생각의 사고를 넓혀 문제를 해결하고 이를 코드로 어떻게
구현할수 있을까를 끊임없이 고민하고 있다.

Github 링크

profile
질문의 질이 답의 질을 결정한다.
post-custom-banner

0개의 댓글