코드 실행 시, "실행 시 검은 화면만 나오는 경우"  혹은 "Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value"의 오류가 발생하는 경우 (로드 과정에서 컴포넌트 속성을 수정하는 경우 ex) label.text = "...")

 

이번 경우에는 화면 이동을 위해 SceneDelegate.swift 내에서, 아래와 같이 코드를 추가해서 문제가 발생함

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let windowScene = (scene as? UIWindowScene) else { return }

    let window = UIWindow(windowScene: windowScene)
    let rootVC = HomeViewController()   // 오류 발생 원인 코드
    let navigationController = UINavigationController(rootViewController: rootVC)

    window.rootViewController = navigationController
    self.window = window
    window.makeKeyAndVisible()
}

 

StoryBoard 기반의 UIKit를 사용하기 때문에, 단순히 저렇게 생성자를 호출하는 방식으로는 VC가 정상적으로 생성되지 않음

아래와 같이 identifier를 기반으로 instantiateViewController 메소드를 호출하여 초기화해야함

// 해결 코드
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let rootVC = storyboard.instantiateViewController(identifier: "HomeViewController") as! HomeViewController

 

 

1. UIKit를 사용하는 새 프로젝트 생성

2. Main.storyboard 파일 삭제

3. info.plistStoryboard Name 제거

 

4. 프로젝트 Target 내 Build Setting UIKit Main Storyboard File Base Name 제거

 

5. SceneDelegate.swift 파일 내 willConnectTo 메소드 내용 수정

* UINavigationController(rootViewController: MainViewController()) 중 'MainViewController'는 처음 시작할 ViewController명으로 지정

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = UINavigationController(rootViewController: MainViewController())
        window?.backgroundColor = .white
        window?.makeKeyAndVisible()
    }
    
}

pod 추가 이후 실행 시 아래와 같은 오류 발생

SDK does not contain 'libarclite' at the path '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a'; try increasing the minimum deployment target

 

podfile 내 platform 관련 코드도 주석 해제 해제 후 11.0으로 변경해주고,

 

iOS Deployment Target 값도 13.0으로 변경해줬음

 

그리고 실행했더니 냅다 또 오류 4개 추가로 뜸

Sandbox: rsync.samba(59873) deny(1) ...(생략)

 

찾아보니 아래와 같이 Build Setting 내 "User Script Sandboxing"의 값을 Yes -> No 로 변경하면 실행 성공함

 

 

 

 

* 참고한 사이트 *

- https://stackoverflow.com/questions/77139617/clang-error-sdk-does-not-contain-libarclite-at-the-path

- https://www.inflearn.com/questions/1057232/error-xcode-sandbox-rsync-13885-deny-1

 

구현 결과

 

구현 코드

https://github.com/ryr0121/UIKitPractice/tree/main/StackViewPractice

 

사용한 라이브러리

Snapkit, Then

 

구현 과정

구현에 참고한 이미지

1. 구현 영역 분리

- 모서리가 둥근 전체 컨테이너 역할의 UIView

- 좌측 상단의 도시명, 현재 기온을 포함한 UIStackView (수직 방향)

- 우측 상단의 구름 이미지, 날씨, 최고/최저 기온을 포함한 UIStackView (수직 방향)

- 하단의 각 시간대별 날씨를 포함한 UIStackView (수평 방향)

 

 

2. 사용될 UI 컴포넌트 정의

2-1. 모서리가 둥근 전체 컨테이너 역할의 UIView 정의

lazy var backView = UIView().then {
    $0.backgroundColor = .gray
    $0.layer.cornerRadius = 20
    $0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapWeatherView)))
}

 

2-2. 좌측 상단의 도시명, 현재 기온을 포함한 수직 UIStackView

 

lazy var leftTitleLabel1 = getLabelView(titleStr: "서울특별시", fontSize: 20.0, fontWeight: .bold, fontColor: .white)
lazy var leftTitleLabel2 = getLabelView(titleStr: "9°", fontSize: 40.0, fontWeight: .medium, fontColor: .white)

lazy var leftStackView = UIStackView().then {
    $0.backgroundColor = .green
    
    // 스택 요소로 포함할 subView들을 선언
    let stackView = UIStackView(arrangedSubviews: [leftTitleLabel1, leftTitleLabel2])
    
    $0.axis = .vertical		// 수직 방향으로 스택을 구성
    $0.distribution = .fill	// 요소들이 스택을 채울 수 있게 분포되도록 구성
    $0.alignment = .leading	// 왼쪽 끝에 붙어서 요소가 나열되도록 구성
    $0.spacing = 0		// 요소 간의 간격 0
}

 
// helper methods - 컨테이너 뷰를 가진 UILabel 컴포넌트를 반환
func getLabelView(titleStr: String, fontSize: Double, fontWeight: UIFont.Weight, fontColor: UIColor) -> UIView {
    let titleLabel = UILabel().then {
        $0.text = titleStr
        $0.font = .systemFont(ofSize: fontSize, weight: fontWeight)
        $0.textColor = fontColor
    }
    let containerView = UIView().then {
        $0.backgroundColor = .systemPink
        $0.backgroundColor = .clear
    }

    containerView.addSubview(titleLabel)
    titleLabel.snp.makeConstraints { make in
        make.top.bottom.leading.trailing.equalToSuperview()
    }
    return containerView
}

 

2-3. 우측 상단의 구름 이미지, 날씨, 최고/최저 기온을 포함한 수직 UIStackView

let cloudImgView = UIImageView(image: UIImage(systemName: "cloud.fill")).then {
    $0.tintColor = .white
}

// getLabelView는 상단 helper method 참조
lazy var rightTitleLabel1 = getLabelView(titleStr: "대체로 흐림", fontSize: 18.0, fontWeight: .medium, fontColor: .white)
lazy var rightTitleLabel2 = getLabelView(titleStr: "최고: 21° 최저 7°", fontSize: 18.0, fontWeight: .medium, fontColor: .white)
    
lazy var rightStackView = UIStackView().then {
    $0.backgroundColor = .blue
    let stackView = UIStackView(arrangedSubviews: [cloudImgView, rightTitleLabel1, rightTitleLabel2])
    $0.axis = .vertical
    $0.distribution = .fill
    $0.alignment = .trailing	// 요소들이 오른쪽 끝에 붙어 나열될 수 있도록 구성
    $0.spacing = 3
}

 

좌우측 상단에 스택뷰를 추가한 후

 

2-4. - 하단의 각 시간대별 날씨를 포함한 수평 UIStackView 

let weatherWithTimeList = [
    ["오전 8시", "10°"],
    ["오전 9시", "12°"],
    ["오전 10시", "15°"],
    ["오전 11시", "17°"],
    ["오후 12시", "19°"],
    ["오후 1시", "20°"],
]

lazy var weatherViews: [UIView] = []

// helper method - 시간 및 기온 정보 배열을 토대로 하단 수평 스택뷰의 요소로 쓰이는 UIView 반환
// viewDidLoad 내에서 호출하여 weatherViews 배열 구성
func configureWeatherOfTime() {
    weatherWithTimeList.map { info in
        weatherViews.append(getWeatherViewOfTime(timeStr: info[0], temper: info[1]))
    }
}

lazy var bottomStackView = UIStackView().then {
    let stackView = UIStackView(arrangedSubviews: weatherViews)
    $0.axis = .horizontal	// 수평 방향으로 요소가 나열되도록 지정
    $0.distribution = .fillEqually	// 모든 요소가 스택 내에서 동등한 크기를 가지도록 지정
    $0.alignment = .leading
    $0.spacing = 5
}

하단 스택뷰를 추가한 후

 

 

3. 레이아웃 구성

private func setLayouts() {
    // add views
    self.view.addSubview(backView)

	// 전체 컨테이너 뷰에 좌측 상단, 우측 상단, 하단 스택뷰를 추가
    backView.addSubview(leftStackView)
    backView.addSubview(rightStackView)
    backView.addSubview(bottomStackView)

    // add views in StackView
    // 좌측 상단 스택뷰 내에 요소로 사용될 sub view들을 추가
    leftStackView.addArrangedSubview(leftTitleLabel1)
    leftStackView.addArrangedSubview(leftTitleLabel2)

    // 우측 상단 스택뷰 내에 요소로 사용될 sub view들을 추가
    rightStackView.addArrangedSubview(cloudImgView)
    rightStackView.addArrangedSubview(rightTitleLabel1)
    rightStackView.addArrangedSubview(rightTitleLabel2)

    // 하단 스택뷰 내에 요소로 사용될 sub view들을 추가
    weatherViews.map { view in
        bottomStackView.addArrangedSubview(view)
    }

    // set constraints
    backView.snp.makeConstraints { make in
        make.top.equalToSuperview().offset(130)
        make.leading.equalToSuperview().offset(20)
        make.trailing.equalToSuperview().inset(20)
    }

    leftStackView.snp.makeConstraints { make in
        make.leading.top.equalToSuperview().offset(15)
    }

    cloudImgView.snp.makeConstraints { make in
        make.top.equalToSuperview()
    }
    rightStackView.snp.makeConstraints { make in
        make.top.trailing.equalToSuperview().inset(15)
        make.bottom.equalTo(leftStackView.snp.bottom)
    }
    bottomStackView.snp.makeConstraints { make in
        make.top.equalTo(leftStackView.snp.bottom).offset(30)
        make.leading.trailing.equalToSuperview().inset(10)
        make.bottom.equalToSuperview().inset(15)
    }
}

 

4. 추가) 수직 스택뷰 요소 간 spacing 달리 주기

// 구름 이미지 UIImageView를 감싸는 컨테이너 뷰 정의
let cloudImgContainerView = UIView().then {
    $0.backgroundColor = .clear
}

let cloudImgView = UIImageView(image: UIImage(systemName: "cloud.fill")).then {
    $0.tintColor = .white
    $0.contentMode = .scaleAspectFill
}

// 레이아웃 구성 변경
cloudImgView.snp.makeConstraints { make in
    make.top.bottom.leading.trailing.equalToSuperview()
}

// 구름 이미지 컴포넌트 하단 요소에 대해 top 방향으로 원하는 만큼의 여백값을 추가
rightTitleLabel1.snp.makeConstraints { make in
    make.top.equalTo(cloudImgView.snp.bottom).offset(20)
}

 

5. 추가) 전체 컨테이너 영역 내에 대한 탭 이벤트 정의

lazy var backView = UIView().then {
    $0.backgroundColor = .gray
    $0.layer.cornerRadius = 20
    
    // 탭 제스처를 감지하는 객체를 추가
    $0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapWeatherView)))
}

@objc func didTapWeatherView() {
    let detailVC = WeatherDetailViewController()
    self.navigationController?.pushViewController(detailVC, animated: true)
}

*참고*

SceneDelegate.swift 파일 내 willConnectTo 메소드

코드 베이스로 UIKit을 사용할 경우,

가장 처음 시작점이 되는 ViewController 내에서 NavigationController를 사용하기 위해서는 

해당 ViewController가 UINavigationContoller를 가질 수 있도록 정의해주어야 함

 

 

 

 

구현 결과

 

구현 코드

https://github.com/ryr0121/UIKitPractice/tree/main/CustomCollectionViewPractice

 

구현 과정

1. Storyboard에 CollectionView 추가

컴포넌트 추가 단축키 - cmd + shift + L

 

2. 추가한 CollectionView를 @IBOutlet으로 연결

 

3. CollectionView에 사용할 커스텀 Cell을 Xib으로 생성 및 정의

  3-(1) CollectionViewCell을 xib으로 생성

파일 추가 단축키 - cmd + N
"Also create XIB file"을 반드시 체크해주어야 .xib 생성됨
생성 완

  3-(2) Cell 내부 정의

우측에 있는 width / height로 셀의 크기를 조정하여 스토리보드 사용 가능
파일을 듀얼로 띄우기 - option을 누른 채로 왼쪽 파일 탭에서 띄울 파일을 클릭

 

4. CollectionView를 사용하기 위한 코드 추가 (프로토콜 채택, 커스텀 셀 등록 등)

class ViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupCollectionView()
    }

    func setupCollectionView() {
        // CollectionView를 사용하기 위한 필수 프로토콜 채택
        collectionView.dataSource = self
        collectionView.delegate = self
        // CollectionView에 사용할 커스텀 셀 등록
        collectionView.register(UINib(nibName: "MainCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "MainCollectionViewCell")
    }
}
extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        // CollectionView에 보여질 셀의 개수 반환
        return 10
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        // CollectionView에 보여질 셀 반환
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MainCollectionViewCell", for: indexPath) as? MainCollectionViewCell else { return UICollectionViewCell() }
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        // 셀의 크기 반환
        return CGSize(width: 100, height: 100)
    }
}

 

 

추가

Storyboard에서 CollectionView의 스크롤 방향 조정하기

Vertical 수직 스크롤 (기본값)
Horizontal 수평 스크롤

 

* 한 줄짜리 수평 스크롤이 가능한 CollectionView를 만들기 위해서는, CollectionView의 높이가 셀의 높이의 2배보다 작아야 함

높이를 130으로 지정한 수평 스크롤이 가능한 CollectionView

구현 결과

구현 코드 - https://github.com/ryr0121/UIKitPractice/tree/main/TableViewPracticeWithStoryboard

 

구현 과정

1. Storyboard에 TableView 추가

컴포넌트 추가 단축키 - cmd + shift + L

 

2. 추가한 TableView를 @IBOutlet으로 연결

 

3. TableView에 사용할 TableViewCell을 xib파일로 정의

   (1) xib 파일 생성하기

새 파일 생성 단축키 - cmd + N
"Also create XIB file"을 반드시 체크!!!해주어야만 .xib이 생성됨
생성 완.

 

   (2) xib 파일 내에 컴포넌트 추가 및 레이아웃 구성

파일 듀얼로 띄우는 법 - option을 누른 채로 옆에 띄울 파일을 클릭

 

4. TableView를 사용하기 위한 사전 작업을 포함한 코드 추가 (프로토콜 채택, 커스텀 셀 등록 등)

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()커

        setupTableView()
    }
    
    func setupTableView() {
        // TableView를 사용하기 위해 필수적인 프로토콜 채택
        tableView.delegate = self
        tableView.dataSource = self
        // TableView에 커스텀 셀을 사용하기 위한 사전 등록
        tableView.register(UINib(nibName: "MainTableViewCell", bundle: nil), forCellReuseIdentifier: "MainTableViewCell")
    }
}
class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        setupTableView()
    }
    
    func setupTableView() {
        // TableView를 사용하기 위해 필수적인 프로토콜 채택
        tableView.delegate = self
        tableView.dataSource = self
        // TableView에 커스텀 셀을 사용하기 위한 사전 등록
        tableView.register(UINib(nibName: "MainTableViewCell", bundle: nil), forCellReuseIdentifier: "MainTableViewCell")
    }
}

결과물

사용 라이브러리 - SnapKit , Then

구현 코드 - https://github.com/ryr0121/UIKitPractice/tree/main/DoubleCVPractice

 

포함 관계

노란색 배경 CollectionView > 파란색 배경 CollectionView > 초록색 배경 View (Cell)

 

구현 과정

1. ViewController안에 노란색 배경의 CollectionView 정의

class ViewController: UIViewController {
	...
	var collectionView: UICollectionView!

	func setupCollectionView() {
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: .init()).then{
            let flowLayout = UICollectionViewFlowLayout()
            flowLayout.itemSize = CGSize(width: self.view.frame.width, height: 100)
            flowLayout.scrollDirection = .vertical
            flowLayout.minimumLineSpacing = 10

            $0.collectionViewLayout = flowLayout
            $0.delegate = self
            $0.dataSource = self
            $0.backgroundColor = .yellow

            $0.showsHorizontalScrollIndicator = false

            $0.decelerationRate = .fast
            $0.isPagingEnabled = false

            $0.register(MainCollectionViewCell.self, forCellWithReuseIdentifier: MainCollectionViewCell.identifier)
        }
	}
	...
}

 

2. ViewController 내에서 CollectionView 관련 프로토콜을 채택하며 cellForItemAt 메소드 내에 새롭게 정의한 CollectionViewCell을 반환시킴

extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MainCollectionViewCell.identifier, for: indexPath) as? MainCollectionViewCell else { return UICollectionViewCell() }
        return cell
    }
}

 

3. 위에서 반환되는 셀(파란색 배경)을 정의하는 CollectionViewCell 클래스 내에서 해당 클래스에 또 다른 CollectionView를 가지도록 정의 및 프로토콜 채택

class MainCollectionViewCell: UICollectionViewCell {
    ...    
    var subCollectionView: UICollectionView!

    func setupCollectionView() {
        subCollectionView = UICollectionView(frame: .zero, collectionViewLayout: .init()).then{
            let flowLayout = UICollectionViewFlowLayout()
            flowLayout.itemSize = CGSize(width: 100, height: 100)
            flowLayout.scrollDirection = .horizontal
            flowLayout.minimumLineSpacing = 10

            $0.collectionViewLayout = flowLayout
            $0.delegate = self
            $0.dataSource = self
            $0.backgroundColor = .systemBlue

            $0.showsHorizontalScrollIndicator = false

            $0.decelerationRate = .fast
            $0.isPagingEnabled = false

            $0.register(SubCollectionViewCell.self, forCellWithReuseIdentifier: SubCollectionViewCell.identifier)
        }
    }
    ...
}

extension MainCollectionViewCell: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 20
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SubCollectionViewCell.identifier, for: indexPath) as? SubCollectionViewCell else { return UICollectionViewCell() }
        return cell
    }
}

 

4. 파란색 배경의 셀이 가지는 CollectionView의 셀을 정의하는 클래스(초록색 아이템)를 정의

class SubCollectionViewCell: UICollectionViewCell {
    static let identifier = "SubCollectionViewCell"

    var containerView = UIView().then { $0.backgroundColor = .green }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        self.contentView.addSubview(containerView)
        
        containerView.snp.makeConstraints { make in
            make.top.leading.bottom.trailing.equalToSuperview().inset(5)
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

 

컬렉션 뷰 셀 내에 세부 내용을 보여줄 라벨과 날짜를 보여줄 라벨 두 가지를 추가하고

날짜 라벨은 날짜 딱 한 줄만 보여주도록, 내용 라벨은 여러줄에 걸쳐 보여줄 수 있도록 제약조건 설정을 해주었다 (각각 모두 12로 맞춰줌)

이 때 hugging priority 관련의 제약조건 오류가 생기는데, Content Hugging Priority가 두 라벨 모두 같은 값이고, 셀 내의 제약조건에 따라 요소들을 위치시키려고 하자니, 어떤 요소를 더 늘려서 맞춰넣어야 하는지 애매해져버려 발생하는 오류인 것 같다

 

Content Hugging Priority값이 더 낮으면, 그 값이 더 높은 요소 대신에 크기가 늘려져서 제약조건에 맞도록 설정된다

값이 더 높은 요소는 정해진 조건값에 따라 그 크기를 유지할 수 있게 된다

 

 

내용 라벨의 Content Hugging Priority 값을 1 낮추어서, 날짜 라벨의 그 값보다 더 낮도록 설정해주면 아래와 같이 오류가 해결된다

 

 


content compression resistance priority는 요소가 가지는 크기가 커지는 경우에 대해, 찌그러질 것이냐 말것이냐에 대한 우선순위를 정하는 것이다

이미지에서의 컬렉션뷰 셀과 같이 정해진 범위내에서 만약 내용 라벨에 담길 컨텐츠가 많아지면, 내용 라벨이 자신의 크기를 줄이던지, 혹은 날짜 라벨이 줄어들던지 해야할 것이다

이 경우에 대해, content compression resistance priority값이 날짜 라벨이 더 높게 설정된다면 날짜 라벨은 크기를 유지하고, 값이 더 낮은 내용 라벨이 찌그러지게(크기가 줄어들게) 되는 것이다

 


 

일단 내가 이해한 바로는,,,

Content Hugging Priority

       -> 자리 남는다; 누가 몸집 더 키울래? (값 높은 애 : 전 아닙니다 / 값 낮은 애 : 아휴 제가 늘릴게요;)

Content Compression Resistance Priority
      -> 뭐야 자리 부족하잖아; 누가 찌그러질래? (값 높은 애 : 전 아니라고요 / 값 낮은 애 : 예예 저요)

+ Recent posts