각각의 기능으로 분리되어 있는 프로젝트들은 공통의 리소스를 가질 수 있다.

Color, Image, Lottie의 JSON 등을 포함한다.

각각의 프로젝트들은 공통의 리소스를 사용하여 개발해야 하기 때문에, 리소스 Framework를 만들어 관리할 수 있다.

중복되는 이미지도 없고, 리소스 Framework를 import만 하면 된다.

 

Image, Color를 리소스 Framework으로 관리

먼저 리소스 Framework를 만든다. 그리고 이미지를 Images.assets에 추가한다.

 

해당 이미지들은 외부에서 코드로 불러와 사용하는 경우가 있기 때문에, 이미지를 외부에서 접근할 수 있는 코드를 만든다.

Resource Framework는 R 이라는 타입으로 접근하여 사용할 것이다.

 

먼저 R 타입을 만들자.

Framework로 만들었기 때문에, 이미지를 불러올 때 Resource Framework의 Bundle 위치를 알기 위해 내부에서 사용할 Bundle을 만들었다.

이제 이미지를 외부에서 접근할 수 있는 코드를 R.Image.[이미지이름] 형태를 따르도록 만든다.

 

 

이제 외부에서는 다음과 같이 이미지를 불러올 수 있다.

 

Color도 Image와 마찬가지로 만들 수 있다.

 

 

Storyboard, Xib를 리소스 Framework에서 관리

iOS 개발시, 항상 논쟁이 되는 주제가 있다. 바로 View를 Storyboard vs Xib로 작성하는 것이다.

 

Storyboard 파일을 갖기 위해서는 Dynamic Framework를 만들어야하는데, 각 기능마다 Framework로 만들게 되면 Framework 개수가 빠른 속도로 늘어날 뿐만 아니라, 기능을 더 작게 나눠 한 화면을 Framework로 만들면 어마어마할 것이다.

 

따라서 Storyboard, Xib를 리소스 Framework에서 관리하면 된다.

리소스 Framework에서 관리하면 화면 단위의 Framework를 Static으로 만들어도 Bundle의 위치가 리소스 Framework이기 때문에 문제가 없다.

그리고 Storyboard, Xib에서 이미지와 Color를 지정해도 Framework 자신의 내부에서 가져오기 때문에 문제가 없다.

 

그렇다면 다른 Framework에 있는 UIViewController Class와 ViewController View를 어떻게 연결할까?

 

위 사진처럼 Module을 선택하고 해당 UIViewController Class 선택하면 된다.

 

사용은

 

 

 

 

 

 

 

 

UIViewController

역할

  • 데이터의 변경에 대한 응답을 View에 Update한다.
  • View와 User와의 상호 작용에 응답한다.
  • View의 크기 조정 및 Layout 관리
  • 다른 객체(viewController 포함)를 조정한다.

간략하게, View와 밀접하게 연결되어 있다.

class UIViewController : UIResponder

또한, UIViewController는 UIResponder를 상속받았다. UIResponder에 대한 이야기는 밑에서 다시 할 것이다.

우선은 SuperView/SubViews 들과의 응답자 체인으로 엮여있다.
이것은 즉

1. 자신의 event의 처리를 SuperView에 passing할 수 있다.

2. 자신의 SubView들의 event를 자신이 처리할 수 있다.

가 가능하다.

 

대부분의 애플리케이션 내에는 여러개의 ViewController가 존재하고 사용된다.

어떤 VC(ViewController)는 리스트를 나타내는 테이블뷰 하나만을 관리할 수 있고,

다른 VC는 리스트, 회원창, 친구목록 등 다양한 View를 관리할 수 있다.

 

UIResponder

앞선 블로그 코코아 프레임워크에서 말했듯, 이 또한 클래스로 NSObject를 상속받는다.

class UIResponder : NSObject
  • Event 처리에 중요한 역할을 한다.
  • 여러 메소드와 next라는 프로퍼티를 사용하여 부모에게 event를 전달하거나, 동시 처리도 가능해진다.
    • touchesBegan
    • touchesMoved
    • touchesEnded
    • touchesCancelled
  • next라는 프로퍼티를 사용하여 다음 responder에게 이벤트를 전달 가능

responder 라고는 주로 becomeFirstResponder() / resignFirstResponder() 를 사용한 기억이 많다.

TextField를 사용하며 키보드를 올리고 내리고 하기 위해 사용해왔지만, 정확히 어떻게 동작하는 지에 대해서는 생각해보질 못했다.

키보드가 올라오기 위해서는 TextField가 window의 첫번째 Responder가 되어야 한다.

메소드이름 그대로 TextField가 첫번째 Responder가 되기 때문에, 키보드가 올라오며, 반대로 Resign하면 내려간다.

 

View 관리

  • 각 VC는 View들이 포함되는 RootView를 관리한다.
  • RootView는 주로 나머지 뷰 계층 구조에서 Container 역할을 한다.
  • VC의 View들은 lazy하게 메모리에 Load된다.
    • 선언한다고 메모리에 적재되는 것이 아닌, 사용할 때(즉, 호출될 때) 메모리에 적재된다.
    • VC 생성 시의 생명주기

StoryBoard를 사용할 때

스토리보드 내에서 1. VC로 사용할 View 와 2. VC 를 지정(연결)한다.

주로 간단한 화면 구조의 애플리케이션이나 혼자서 작업할 때 사용되는 방법이다.

// 스토리보드를 통한 VC 초기화
let newVC = self.storyboard?.instantiateViewController(withIdentifier: "VC이름")

 

Nib  파일을 사용할 때

Nib파일을 사용하여 VC에 대한 View를 지정한다.

스토리보드를 사용하지 않기에, 스토리보드 내에서 가능했던 VC간의 Segue나 관계를 사용/정의할 수 없다.

초기화 방법 : init(nibName:bundle:)을 사용해서 초기화한다.

 

이후의 View들의 Load

VC의 생명주기 메소드들 중 가장 첫 단계인 loadView() 메소드를 사용하여 나열된다.

이후에 DidLoad, willAppear, DidAppear ... 

 

VC의 크기(RootView의 크기)

VC의 RootView는 항상 할당된 공간에 크기가 맞춰진다. 또한 RootView 내에 있는 SubView들은 Interface Builder를 사용하여 각 뷰와 SuperView와의 관계를 통해 크기, 위치 등이 지정된다.

 

구현 방식

1. AutoLayout - View 사이의 관계, 제약조건 등을 통하여 크기, 위치 등을 설정하며, 기기 크기에 대해 대응 가능

2. Frame - CGSize(크기), CGRect(위치)를 직접 할당한다.

 

 

View 관련 Notification

위에 사진으로 나타낸 VC의 생명주기의 메소드이름을 통해 View들이 어떤 상태인지를 알 수 있다.

반대로 말하면, View들의 현재 상태를 VC를 통해 알 수 있다.

 

Segue로 ViewController 전환

  1. StoryBoard 내의 버튼에 Segue 추가하기
  2. StoryBoard 내의 VC에 Segue 추가하고 Segue Identifier 지정하기
  3. StoryBoard 내의 VC에 Identifier 지정하기

1번 방법

버튼으로 Segue추가

위와 같은 방법으로 Tap했을 때 Segue를 발생시키고 싶은 버튼을 Ctrl+드래그 한다.

해당 Button을 클릭했을 때, 이동하고 싶은 VC에 드래그한다.

 

이렇게 설정해놓으면 EditButton을 Tap하게 되면 LoginVC로 이동하는 것을 볼 수 있다.

 

데이터 전달

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if let loginVC = segue.destination as? LoginViewController{
                loginVC.nameText = self.nameLabel.text ?? "name"
                loginVC.descriptionText = self.descriptionLabel.text ?? "description"
            }
        
    }

Presenting VC에서 prepare 메소드를 재정의한다.

destination은 Presented VC가 되고 옮기고 싶은 데이터들을 옮겨주면 된다.

이때, LoginViewController는 아직 메모리에 올라가지 않았기 때문에, IBOutlet 에는 접근할 수 없다.


2번 방법

VC에서 Swgue 추가

위와 같은 방법으로 Segue를 발생시키고 싶은 VC를 Ctrl+드래그한다.

해당 VC에서 이동하고 싶은 VC에 드래그한다.

Segue Identifier

생성된 Segue에 Identifier를 지정해준다.

나는 Profile에서  Login으로 가는 Segue이기 때문에, ProfileToLogin이라고 지었다.

이때, 복잡한 UI와 애니메이션이 들어가는 App을 개발할 때는 Modal방식인지 직접 커스텀한 애니메이션을 가진 방식인지에 따라서 네이밍을 더 복잡하게 하기도 한다.

@IBAction func tappedEditButton(_ sender: Any) {
        performSegue(withIdentifier: "ProfileToLogin", sender: nil)
        
    }

EditButton을 눌렀을 때, 이동시키기 위해 IBAction에 performSegue를 넣어주었다.

이때, withIdentifier에는 위에서 네이밍한 Swgue Identifier를 적어주면된다.

 

EditButton을 Tap하게 되면 LoginVC로 이동하게 된다.

 

데이터 전달

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "ProfileToLogin"{
            if let loginVC = segue.destination as? LoginViewController{
                loginVC.nameText = self.nameLabel.text ?? "name"
                loginVC.descriptionText = self.descriptionLabel.text ?? "description"
            }
        }
        
    }

또한, Presenting VC에서 하나의 Segue만 가지고 있을 때는 상관없는데, 만약 2개 이상의 Segue를 처리하게 된다면

prepare에서 발생한 Segue의 Identifier를 확인하여 원하는 데이터 전달을 구현할 수 있다.

 

 


3번 방법

@IBAction func tappedEditButton(_ sender: Any) {
        if let loginVC = self.storyboard?.instantiateViewController(withIdentifier: "LoginViewController") as? LoginViewController{
            present(loginVC, animated: true, completion: nil)
        }
    }

코드만 사용해서 EditButton을 Tap했을 때 LoginVC로 이동하게 된다.

 

이렇게만 해서는 안되고 한단계가 더 있다.

 

위의 withIdentifier에 들어가는 값은 LoginViewController의 타입이 아닌 String이다!

눈치 빠른 분들은 아셨겠지만, Segue에 Identifier를 설정한 것처럼 VC에도 Identifier를 설정가능하다.

VC Identifier 설정

그림처럼 VC에 Identifier를 설정하면 된다.

데이터 전달은 present(...)하기 전에 데이터를 전달하면 된다.

StoryBoard vs Code

iOS 프로젝트를 처음 그리고 초반에 진행을 할 때는 무조건 스토리보드로 UI를 작성하고 ViewController와 연동하였다.

why?

쉬우니까! 간편하니까! 내가 작성한 것이니 내가 제일 잘 아니 헷갈리지도 않으니까!

BUT!

혹시라도 2명이상과 프로젝트를 진행하게 된다면 스토리보드의 가장 큰 단점이 보인다.

  • 하나의 스토리보드로 함께 작업시에 충돌할 가능성이 진부함
  • 스토리보드를 나눠서 작업을 해도 굳이 하나의 스토리보드에 들어갈 화면들이 나눠짐 -> 이후에 다시 합쳐야 하는 불편함
  • 이후에 다시 합치는 과정에서도 Ctrl+C/V를 하더라도 다시 AutoLayout을 위한 Constraint를 지정해주는 것이 어렵다!
    • 물론 정리된 문서가 있으면 금방할 수 있다고 생각했지만, 아니었다...
    • 내가 UI Component들에 지정한 이름을 보고 이게 이거였나? 이게 이거였겠지? 하면서 헷갈리는 순간 시간이 굉장히 딜레이됐다.
    • 그래서 이후로는 UI Component들의 이름도... 어떠한 체계로 명확히 작성해야한다는 것을 꺠달았따...

TabBar

예제로 진행할 탭바 StoryBoard

목표 : 이처럼 TabBarVC를 가지고 있고 2개의 VC를 삽입하여 간단한 TabBar를 구현하는 것!!

 

class TabBarViewController: UITabBarController{
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let profileVC = ProfileViewController()
        let vc2 = VC2()
        
        let icon1 = UITabBarItem(title: "프로필", image: UIImage(systemName: "person.circle"), selectedImage: UIImage(systemName: "person.circle"))
        let icon2 = UITabBarItem(title: "VC2", image: UIImage(systemName: "person.circle"), selectedImage: UIImage(systemName: "person.circle"))
        
        profileVC.tabBarItem = icon1
        vc2.tabBarItem = icon2
        let vcArr = [profileVC, vc2]
        self.setViewControllers(vcArr, animated: true)
    }
        
}

 

  1. TabBarVC라는 ContainerVC에 들어갈 ProfileVC, VC2를 생성한다.
  2. 각각의 VC들이 TabBar에서 보일 Item을 만든다.
  3. 2번에 이어서 붙여준다.
  4. VC들을  TabBarVC의 VC들로 지정한다.

이과정을 진행하면 문제없이 시뮬레이터에서 실행이될 것이다.

ProfileVC
VC2

보라! 정상적으로 TabBar가 구현되었다!!

응??? 근데 뭔가 이상하다??

스토리보드에 있는 ProfileVC

스토리보드에 있는 ProfileVC에는 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ라고 적힌 Label이 있는데??

아! ProfileVC에 Outlet으로 가져오면 되겠구나!

 

class ProfileViewController: UIViewController {
    
    @IBOutlet weak var label: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.label.text = "aa"
    }


}

하고 실행!

Outlet을 지정한 후의 ProfileVC

엥? 아직도 안뜨네? 거기다가

Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value 라는 에러도 나타났다.

이 에러는 타입에 !를 선언해주고 nil에 접근했을 때 나타나는 건데....

스토리보드에서 profileVC와 ProfileViewController를 연결하고 Outlet에 Label을 연결했고! 아무 문제가 없어야하는데 문제가 발생??




해결 방법

guard let profileVC = self.storyboard?.instantiateViewController(withIdentifier: "ProfileVC") else { return }

위 코드처럼 바꿔주면된다.

해석하자면

  1. self가 존재하는 스토리보드에 있는 VC들 중에 "ProfileVC"라는 ID를 가진 VC를 가져온다.

스토리보드에 UI Component들을 작업하고 그 컴포넌트들을 코드로 생성한 VC에서 사용하고 싶으면 이런 방법을 써야한다.

물론 스토리보드에 UI Component들을 넣지 않고 코드로

let label = UILabel() 로 선언한 후에

text와 constraint들을 지정해 줘도 되지만... 아직까지는 그렇게 익숙하지 않으니 이 방법도 알아보게 되었다.

+ Recent posts