iOS Target에 대한 정의, 분리하는 방법, 분리하여 사용했을 때 마주했던 이슈(Target Membership)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


Target

기본적으로 회사에서는 여러 타겟을 나눠서 사용을 하고 있을것이고 이렇게 만들어진 타겟은 빌드할 때 각자 빌드하는 타겟이 설정됩니다.

타겟에 대한 문서 설명 먼저 봐보자!

A target specifies a product to build and contains the instructions for building the product from a set of files in a project or workspace. A target defines a single product; it organizes the inputs into the build system—the source files and instructions for processing those source files—required to build that product. Projects can contain one or more targets, each of which produces one product.

이 문서를 직역해보자면, 타겟은 Xcode 프로젝트를 빌드할 때 빌드 과정에서 어떤 리소스, 소스파일들을 포함할 지 설정할 수 있고 빌드 과정의 순서 등 프로젝트의 큰 설정을 지정할 수 있는 것을 의미하는 것 같다. 그리고 이때 중요한 점은 하나의 타겟이 하나의 프로덕트인 것으로 즉, 타겟에 따라 하나의 프로젝트에서 여러 버전으로 프로덕트를 분리할 수 있는 것이다. 그렇기 때문에 기본적으로 회사에서는 Qa, Dev, DevTest 이런 식으로 각각의 타겟이 분리되어 있을 것이다.

즉 간단하게 타겟의 특징을 설명하자면,

  1. 하나의 타겟은 하나의 프로덕트이다.
  2. 하나의 프로젝트는 여러개의 타겟(프로덕트)로 이루어질 수 있다.
  3. 타겟별로 빌드 설정을 다르게 할 수 있다.

Target 분리하기

타겟 분리하는 실습을 해보기 위해 TargetProject라는 새 프로젝트를 생성해보았다.

타겟을 분리하는 방법은 아래와 같다.

1. Target 우클릭 > Duplicate 선택

그러면 아래 이미지와 같이 복사본 타겟이 하나 생성됩니다.

2. 복사된 버전을 원하는 이름으로 변경

이후, 이 복사된 버전을 원하는 이름으로 설정해줍니다.
저는 아래와 같이 TargetProjectDev로 이름을 변경해 주었습니다.

3. 빌드 환경에서 복사된 target 있는지 확인

4. Info.plist 설정

새롭게 Target을 생성했을때 Info.plist 역시 복사가 되어 자동으로 생성됩니다.
그러나 이름은 아래와 같이 TargetProject copy-info.plist로 되어있죠. 원하는 이름으로 수정해봅니다.

그리고 수정한 이름을 Xcode 프로젝트에서도 인식을 해줘야 하기에 Target > Build settings > Packaging 에서 변경한 이름으로 똑같이 변경해줍니다.

이렇게 완료한다면 이제 각각 타겟에 대한 설정은 어느정도 완료되었습니다.
이후부터는 이제 각자의 프로젝트 개발 상황에 맞게 설정을 해주면 됩니다.

이렇게 나뉜 타겟이 제대로 설정되었는지 확인해 봅시다.

5. Target 이름으로 구분해 빌드시 간단한 로그 확인해보기

만들어준 타겟의 Build settings > Swift Compiler - Custom Flags 에서 전처리문에서 구분하고 싶은 이름으로 변경해줍니다.
주의할 점은 꼭 -D라는 문장을 전에 입력해 주어야 인식이 가능하다는 점입니다.

이제 프로젝트로 돌아가 확인하고 싶은 뷰컨트롤러에서 다음과 같은 코드를 입력해주세요.

#if DEV
print("I'm Dev Target")
#else
print("I'm not Dev Target")
#endif

그러면 상황에 따라 이렇게 로그가 찍히는 것을 볼 수 있을 것 입니다!


Target을 분리하여 사용했을 때 마주했던 이슈

타겟을 분리해서 사용하던 중 A라는 뷰 컨트롤러를 생성해 코드를 진행하였고 이 A 뷰 컨트롤러를 B 뷰 컨트롤러에서 불러오려고 하였습니다. 간단한 예시 코드입니다.

A viewController

final class PracticeViewController: UIViewController {
    static func instance() -> PracticeViewController? {
        return PracticeViewController()
    }
}

B viewController

func pushPraticeVC() {
  guard let viewController = PracticeViewController.instance() else { return }
  self.navigationController?.pushViewController(viewController, animated: true)
}

그런데 이때 이와같은 에러가 발생하였습니다.

Cannot find 'PracticeViewController' in scope

아무리 찾아봐도 이유를 찾을 수 없었습니다.
그러는 와중에 해당 아티클을 보았습니다.

Cannot find type in scope after re-generating Core Data models

문제는 이와 같습니다.

A 뷰컨트롤러의 Target Membership과 B 뷰컨트롤러의 Target Membership이 달랐습니다.
그러니 B 뷰컨트롤러에서는 A 뷰컨트롤러에 접근을 할 수 없었던 것이죠.

아티클의 답변에서도 나왔듯, 보통을 뷰 컨트롤러를 만들때 자동으로 target Membership이 설정되는데, 이 새로운 파일에서는 이와같은 동작이 되지 않은 것입니다.
이를 해결하기 위해서는 아래와 같이 Target Membership 에서 직접 설정해주면 간단하게 해결 됩니다.

따라서 타겟을 분리해 사용할 때에는 이렇게 Target Membership을 한번씩 확인해 주는 습관!! 꼭 가지자!

iOS 코드로 UITableView 구현해보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


코드로 UITableView 구현

개인적으로 스토리보드를 애용해 코드로 UI를 짜본적은 없었는데, 회사에서는 UI를 모두 코드로 진행하고 있었다.
그중 좀 흥미로웠던 부분이 있어 정리해보려고 합니당.

코드로 테이블뷰를 만드는것에 있어서 저는 기본 뷰컨트롤러에 테이블뷰를 추가하고, 테이블 뷰 셀을 가져오는 형식으로 구현할 것입니다.

private let tableView: UITableView = {
   let tableView = UITableView(frame: .zero, style: .grouped)
   tableView.register(PracticeChatTableViewCell.self, forCellReuseIdentifier: PracticeChatTableViewCell.identifier)
   tableView.separatorStyle = .none
   tableView.rowHeight = UITableView.automaticDimension
   tableView.estimatedRowHeight = 150
   return tableView
}()

뷰 컨트롤러에 변수로 테이블뷰를 만드는 방식이다.
이렇게 변수로 만들어놓은 테이블뷰를 화면에 위치 시켜야겠죠? 저는 snapKit을 사용했습니다.

func initViews() {
    super.initViews()

    self.view.addSubview(self.tableView)
    self.tableView.make { (make) in
        make.edges.equalToSuperview()
    }

    self.tableView.delegate = self
    self.tableView.dataSource = self
}

이렇게 만든 initView를 viewDidLoad에서 호출해주는 것 입니다.
그리고 만들어놓은 테이블뷰 변수는 addSubview를 통해 superView와 같은 크기로 위치시켜줍니다.

그리고 웨에 적인 PracticeChatTableViewCell 파일을 만들어줍니다.

import UIKit

class PracticeChatTableViewCell: UITableViewCell {
    static let identifier = "PracticeChatCell"

    private let containerView: UIView = {
        let containerView = UIView()
        containerView.backgroundColor = .red
        return containerView
    }()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        self.contentView.addSubview(self.containerView)
        self.containerView.make { (make) in
            make.edges.equalToSuperview()
        }
    }

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

여기서 가장 흥미로웠던 점은 셀을 만들때 init을 생성해줘야한다는 부분이었다.

일반적으로 셀 파일에서는 앞서 뷰컨트롤러에서 했듯이 initView 함수를 만들어놓고 각 객체들의 설정들을 다뤄줬었는데(스토리보드 사용시) 코드로 이를 진행할 때에는 반드시 init을 생성해줘야한다. 그 이유는 인터페이스 빌더에서는 자동으로 이 객체들을 초기화 해주지만, 코드에서는 인터페이스 빌더를 사용하는 것이 아니기 때문에 직접 초기화를 해줘야 하는 것이다. 이렇게 초기화를 해주지 않으면 아무것도 뜨지 않는다.

실제로 아무생각없이 init을 안해줬더니 정말 아무것도 뜨지 않았다..!

뷰 컨트롤러의 전체 코드는 아래와 같다.

import UIKit

final class PracticeViewController: UIViewController {
    static func instance() -> PracticeViewController? {
        return PracticeViewController()
    }

    private let tableView: UITableView = {
        let tableView = UITableView(frame: .zero, style: .grouped)
        tableView.register(PracticeChatTableViewCell.self, forCellReuseIdentifier: PracticeChatTableViewCell.identifier)
        tableView.separatorStyle = .none
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 150
        return tableView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        initViews()
    }

    func initViews() {
        super.initViews()

        self.view.addSubview(self.tableView)
        self.tableView.make { (make) in
            make.edges.equalToSuperview()
        }

        self.tableView.delegate = self
        self.tableView.dataSource = self
    }
}

extension PracticeViewController: UITableViewDelegate, UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: PracticeChatTableViewCell.identifier, for: indexPath)
        return cell
    }
}

Xcode Storyboard 삭제 및 Scene delegate 삭제하는 방법

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


회사에서는 모든 UI를 코드로 짜고있다. 스토리보드를 무척 사랑했던 나로서는 정말 슬픈일이었지만, 어쩌겠나요!
앞으로는 코드로 UI를 짜는것이 습관화 되어야 하기에 개인 실습 프로젝트를 하나 파게 되었고
그러면서 XCode에서 스토리보드와 Scene delegate를 샂게하는 방법을 같이 정리해보려고 합니다!

Storyboard 삭제하기

스토리보드를 제거하기 위해서는 프로젝트가 생성되는 Main.Storyboard와의 연동된 부분을 끊으면 된다.

Info.plist에서 Main Storyboard file base name > Main을 지운다.

혹은 프로젝트 설정에서 [Main Interface]에서 Main을 지우면 Info.plist에도 반영된다. 이 방법을 사용해도 같이 반영된다.

Main.Storyboard 파일을 삭제한다.

해당 파일은 더이상 사용하지 않기 때문에 삭제해도 괜찮다.

SceneDelegate 삭제하기

기존에 SceneDelegate에서 UIWindow를 설정하는 부분을 예전처럼 AppDelegate로 옮기고, Scene관련 파일과 설정을 제거한다.

AppDelegate 에서 Scene 관련 함수 정의부를 제거한다.

// MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }

AppDelegate에 UIWindow 설정 로직을 추가한다.

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        window = UIWindow()
        window?.rootViewController = ViewController()
        window?.makeKeyAndVisible()

        return true
    }    
}

SceneDelegate 파일을 삭제한다.

해당 파일은 더이상 사용하지 않기에 삭제하도록 한다.

Info.plist에서 Application Scene Manifest 항목을 통쨰로 삭제한다.

확인해보기

ViewController의 기본뷰에 배경색을 입히고 앱을 실행시켜 적용한 배경색이 잘 뜨는지 확인해보자.

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        view.backgroundColor = .blue
    }
}

자주 사용하지만 헷갈리는 git 명령어 정리해보기

|

개인적인 연습 내용을 정리한 글입니다.
더 좋은 방법이 있거나, 잘못된 부분이 있으면 편하게 의견 주세요. :)


git branch 생성하기

git branch [만들 브랜치 이름]

git branch 확인하기

git branch -r  // 원격 저장소 브랜치 리스트 확인
git branch -a  // 원격, 로컬 모든 저장소의 브랜치 리스트 확인

git branch 가져오기

git checkout -t [가져올 브랜치 경로/이름]

git branch 이름 변경하기

git branch -m [기존 브랜치 이름] [바꾸고 싶은 브랜치 이름]

iOS DZNEmptyDateSet 라이브러리 사용해보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


DZNEmptyDataSet

테이블 뷰나 컬렉션 뷰 등에 데이터가 없을 때, 보여줄 수 있는 심플한 화면을 손쉽게 관리할 수 있는 라이브러리

사용이유

  1. 데이터가 없을때의 단순히 흰색 화면을 피학 화면이 비어있는 이유를 사용자에게 전달 가능
  2. 일관성 유지 및 사용자 경험 개선 제공
  3. 브랜드 존재감 제공

Features

  • UITableView, UICollectionView와 호환이 된다. 뿐만 아니라 UISearchDisplayController, UIScrollView와도 호환가능
  • 이미지, 제목 및 설명 레이블, 버튼을 표시함으로써 레이아웃 및 모양의 다양성을 제공
  • NSAttributedString 또한 사용가능
  • 오토레이아웃을 사용함으로써 회전과 함께 콘텐츠를 자동으로 뷰의 중앙에 배치시켜준다. > 수직, 수평 정렬을 허용
  • 배경색상 또한 사용자 정의 가능
  • 테이블뷰 탭 제스처 허용
  • 스토리보드와 호환 가능
  • iOS6, tcOS9 이상부터 호환가능, iPhone, iPad, Apple TV와 호환 가능

해당 라이브러리는 UITableView, UICollectionView 클래스를 확장할 필요 없는 방식으로 설계되어있다.
UITableViewController, UICollectionViewController를 사용할 때 여전히 작동이 가능하다.

DZNEmptyDataSetDelegate, DZNEmptyDataSetSource 만 준수한다면 애플리케이션의 내용과 빈 상태의 모양을 완전히 사용자 지정 가능하다.

사용해보기

pod 'DZNEmptyDateSet'

viewcontroller

class ViewController: UIViewController {
  func viewDidLoad() {
    self.viewDidLoad()

    self.tableView.emptyDataSetSource = self
    self.tableView.emptyDataSetDelegate = self
  }
}

// MARK: DZNEmptyDataSetDelegate
extension ViewController: DZNEmptyDataSetDelegate {
    // 스크롤 권한 요청 > default는 false
    func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView!) -> Bool {
        return true/false
    }
}

// MARK: DZNEmptyDataSetSource
extension ViewController: DZNEmptyDataSetSource {
    // 비어있는 상태의 이미지 설정
    func image(forEmptyDataSet scrollView: UIScrollView!) -> UIImage! {
        return
    }
    // 비어있는 상태의 제목 설정
    func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! {
        return
    }
}