호댕의 iOS 개발

[TWL] 22.01.10 ~ 22.01.15 (FileManager, NSAttributedString, xib 파일, SegmentedControll 등) 본문

Software Engineering/TIL

[TWL] 22.01.10 ~ 22.01.15 (FileManager, NSAttributedString, xib 파일, SegmentedControll 등)

호르댕댕댕 2022. 1. 22. 14:09

FileManager

⌨️ 알아두면 좋을 단축키

  • cmd + shift + . → 숨김 파일 보기
  • cmd + shift + g → 폴더 찾기
  • 폴더 생성 → touch 파일명.확장자 / echo “내용" > 파일명.확장자명

📦 사용한 메서드 정리

디렉토리의 주소를 찾는 경우

  • urls(for: in:) : FileManager의 인스턴스 메서드
    • directory: 찾을 path directory를 작성한다. 다양한 SearchPathDirectory를 가지고 있다.
    • domainMask: 찾을 file system의 도메인을 작성한다.
      • userDomainMask: 사용자의 홈 디렉토리
      • localDomainMask: 기기에서 모든 사람이 item을 설치할 수 있는 곳
      • networkDomainMask: 네트워크에서 item을 설치할 수 있는 곳
      • systemDomainMask: 애플에서 제공하는 시스템 파일을 위한 디렉토리
      • allDomainMask: 모든 도메인
  • FileManager.default : 싱글톤처럼 전역에서 사용할 수 있는 Filemanager 객체

새로운 SubDirectory를 생성하는 경우

  • appendingPathComponent(_:) : 주어진 경로의 구성요소(URL)에 path component를 추가한 후 URL을 반환함
    • pathComponent: 추가할 path 구성 요소
  • createDirectory(at:withIntermediateDirectories:attributes:) : 주어진 attribute에 따라 디렉토리를 생성하는 메서드
    • url: 생성할 디렉토리를 지정해줌, 이는 반드시 nil이 되면 안됨.
    • createdIntermediates: true면 url에 디렉토리를 만드는 과정 중에 존재하지 않는 부모 디렉토리를 생성함. false라면 중간 상위 디렉토리가 존재하지 않는 경우 메소드가 실패함.
    • attributes: 새로운 디렉토리의 파일 속성(소유자 및 그룹 번호, 파일 권한, 수정일 등)을 정해준다. nil로 해주면 umask(2) macOS Developer Tools Manual Page에 따라 정해진다.

파일에 새로운 내용 작성

  • write(to:atomically:encoding:) : 받은 URL에 지정한 encoding 방법을 통해 콘텐츠를 작성함.
    • url: 작성할 컨텐츠를 받을 주소로 file URLs만 지원한다.
    • useAuxiliaryFile: true면 auxiliary file에 기록하고 auxiliary file의 이름이 url로 바뀜. false면 url에 직접 기록을 하게 됨. true를 주면 url이 존재하는 경우 작성하는 동안 시스템이 충돌하게 되더라도 기록이 손상되지 않는 것을 보장.
    • enc: 출력에 사용할 인코딩 방법을 정함

파일을 읽기

  • contents(atPath:) : 지정한 path의 파일 컨텐츠를 반환해줌. FileManager의 인스턴스 메서드

파일을 삭제

  • removeItem(at:) : 지정한 URL의 파일 혹은 디렉토리를 삭제. FileManager의 인스턴스 메서드

 

NSAttributedString

Label.attributedText의 타입으로 단순히 Text의 경우 String이라 폰트를 지정해줄 수 없지만 NSAttributedString의 경우 폰트 및 컬러 등을 부여할 수 있다.

NSAttributedString(string:attributed:)

  • string: 작성할 텍스트를 String 타입으로 작성함.
  • attributed: 텍스트에 적용할 속성을 정의한다. 이는 딕셔너리 형태로 작성해주면 된다.
extension Product {
    var attributedTitle: NSAttributedString {
        return NSAttributedString(
            string: name,
            attributes: [.font: UIFont.preferredFont(forTextStyle: .headline)]
        )
    }
    var attributedPrice: NSAttributedString {
        let result = NSMutableAttributedString()
        if self.bargainPrice == 0.0 {
            let formattedPrice = price.formatted ?? price.description
            let price = NSAttributedString(
                string: currency.rawValue + " " + formattedPrice,
                attributes: [
                    .font: UIFont.preferredFont(forTextStyle: .body),
                    .foregroundColor: UIColor.systemGray
                ]
            )
            result.append(price)
            return result
        }
        let formattedOriginalPrice = price.formatted ?? price.description
        let originalPrice = NSAttributedString(
            string: currency.rawValue + " " + formattedOriginalPrice,
            attributes: [
                .font: UIFont.preferredFont(forTextStyle: .body),
                .foregroundColor: UIColor.systemRed,
                .strikethroughStyle: NSUnderlineStyle.single.rawValue
            ]
        )
        let formattedBargainPrice = bargainPrice.formatted ?? bargainPrice.description
        let bargainPrice = NSAttributedString(
            string: currency.rawValue + " " + formattedBargainPrice,
            attributes: [
                .font: UIFont.preferredFont(forTextStyle: .body),
                .foregroundColor: UIColor.systemGray
            ]
        )
        let blank = NSAttributedString(string: " ")
        result.append(originalPrice)
        result.append(blank)
        result.append(bargainPrice)
        return result
    }
    var attributedStock: NSAttributedString {
        if stock == 0 {
            let outOfStock = NSAttributedString(
                string: "품절",
                attributes: [
                    .font: UIFont.preferredFont(forTextStyle: .body),
                    .foregroundColor: UIColor.systemOrange
                ]
            )
            return outOfStock
        }
        let currentStock = NSAttributedString(
            string: "잔여수량 : \\\\(stock)",
            attributes: [
                .font: UIFont.preferredFont(forTextStyle: .body),
                .foregroundColor: UIColor.systemGray
            ]
        )
        return currentStock
    }
}

xib 파일

xib 파일은 Cocoa Touch Class로 파일을 생성할 때 아래 체크 박스를 선택하면 생성할 수 있다.

xib 파일을 처음 사용해봐서 헤맸었다... 일반 스토리보드를 사용할 때보다 약간 다른 부분이 존재했다. 만약 TableView와 TableViewCell일 경우 이 둘을 연결시켜줘야 한다.

이 작업은 TableViewController의 viewDidLoad에서 해주면 된다.

let nibName = UINib(nibName: "ProductsTableViewCell", bundle: nil)
tableView.register(nibName, forCellReuseIdentifier: reuseIdentifier)

segmentedControll

각 세그먼트에 해당하는 버튼을 눌러 세그먼트에 맞는 화면을 보여주는 기능이다.

UISegmentedControl에 정의되어 있는 selectedSegmentIndex는 사용자가 마지막으로 터치한 인덱스를 알려주는 프로퍼티이다.

기본적으로 왼쪽 세그먼트가 0, 오른쪽이 1로 구성되어 있다.

    private func loadViewController() {
        children.forEach { children in
            children.removeFromParent()
        }
        view.subviews.forEach { subView in
            subView.removeFromSuperview()
        }
        if viewSegmetedControl.selectedSegmentIndex == 0 {
            guard let controller = storyboard?.instantiateViewController(
                withIdentifier: listViewControllerIdentifier
            ) else { return }
            addChild(controller)
            view.addSubview(controller.view)
        } else if viewSegmetedControl.selectedSegmentIndex == 1 {
            guard let controller = storyboard?.instantiateViewController(
                withIdentifier: gridViewControllerIdentifier
            ) else { return }
            addChild(controller)
            view.addSubview(controller.view)
        }
    }

시뮬레이터 초기화하기

simulator > device > erase All Content and Settings...를 클릭하면 시뮬레이터를 초기화할 수 있다. 다만 전체 시뮬레이터가 초기화되는 것은 아니고, 해당 기기만 초기화된다.

UIActivityIndicatorView

흔히 로딩 중일 때 돌아가는 것을 구현할 수 있다.

  • .startAnimating: ActivityIndicator를 동작하게 함.
  • .stopAnimating: ActivityIndicator의 동작을 멈추고 숨기게 함. 기본적으로 isHidden 프로퍼티는 true로 되어 있다.

 

Cell의 크기를 자동으로 조정하는 방법

기존에는 아래의 그림처럼 TableView의 Cell의 크기가

automatic

으로 되어 있어서 Cell의 내부 컨텐츠가 변한다면 자동으로 셀의 크기도 변동이 된다고 생각했다.

하지만 실제론 Accessibility를 통해 텍스트의 크기를 늘린 결과 셀의 크기는 정말 아주 약간만 증가했고 레이블이 겹치는 문제가 있었다. 또한 줄바꿈이 되도록 구현을 했었는데 글씨도 아예 잘려버렸다.

이때는 .autoDimension을 rowHeight에 정해주고 estimatedRowHeight를 정해주면 셀의 콘텐츠에 따라 셀의 크기가 변하였다.

tableView.rowHeight =  UITableView.automaticDimension
tableView.estimatedRowHeight = 80.5

Pagination

현재는 페이지 1번에 아이템이 20개만 들어올 수 있도록 구현을 했었다. 따라서 딱 20개까지만 뷰가 있었고 그 이후의 데이터는 볼 수 없는 문제가 있었다.

그래서 UIScrollViewDelegate에 있는 인스턴스 메서드인 scrollViewDidScroll을 사용해서 사용자가 스크롤을 할 때 현재 있는 contentoffset을 계산하는 방식으로 Pagination을 구현했다.

UICollectionViewDelegate, UITableViewDelegate 모두 UIScrollViewDelegate를 상속하고 있어 따로 이를 채택하지 않아도 override로 사용할 수 있었다.

하지만 이 경우 셀의 높이 자체가 변경되거나 할 때에는 제대로 대응을 할 수 없는 문제가 있었다.

extension ProductsTableViewController {
    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard pageInformation?.hasNext == true else { return }
        let contentOffsetY = scrollView.contentOffset.y
        let tableViewContentSize = tableView.contentSize.height
        let frameHeight = scrollView.frame.height

        if contentOffsetY > tableViewContentSize - frameHeight {
            guard let num = pageInformation?.pageNumber else { return }
            loadProductsList(pageNumber: num + 1)
        }
    }
}

오늘 린생 덕분에

override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        <#code#>
    }

이런 메서드를 사용하여 indexPath에 따라 pagination을 구현할 수 있다는 것을 알 수 있었다. 내일은 이 메서드를 활용하여 pagination을 수정해봐야겠다.

Cache

오늘 활동학습 시간에 Alamofire와 Kingfisher의 코드를 직접 보며 어떻게 구현을 했고 어떤 cocoa layout을 사용했는지 확인해봤다. 하지만 아직 정리가 덜 되서 공부를 좀 더 해봐야 할 것 같다.

 

instantiateInitialViewController(identifier:creator:)

이는 iOS 13에서 새롭게 나온 메서드로 스토리보드에서 새로운 ViewController를 생성하고 creator를 통해 의존성 주입도 가능한 메서드이다.

만약 iOS 13 이전의 디바이스에서 이런 기능을 구현하려면 아래 코드처럼 프로퍼티 주입을 사용해야 했다.

// SomeViewController

private var someDependency: SomeDependency!

static func instantiate(someDependency: SomeDependency) -> SomeViewController {
  let viewController = ... (스토리보드로부터 뷰 컨트롤러 생성)
  viewController.someDependency = someDependency
  return viewController
}

또한 Creator를 사용하려면 생성하려는 ViewController에 initializer를 만들어줘야 하는데 이때 required init을 만들라는 에러가 발생했다.

init을 따로 지정해주지 않는 경우 부모 클래스의 이니셜라이저를 자동으로 상속하기 때문에 required init(coder:)이 필요가 없다. 하지만 직접 이니셜라이저를 작성하게 되면 부모 클래스의 이니셜라이져를 자동으로 상속하지 않기 때문에 직접 이를 구현하라는 오류 문구가 나오는 것이다.

이는 UIViewController가 채택하고 있는 NSCoding 프로토콜을 확인해보면 명확하게 알 수 있다. NSCoding의 정의부를 보면

public protocol NSCoding {

    func encode(with coder: NSCoder)

    init?(coder: NSCoder) // NS_DESIGNATED_INITIALIZER
}

이렇게 실패 가능한 이니셜라이저로 init(coder:)를 지정해놨기 때문에 이를 채택한 곳에서도 필요하게 되는 것이다.

그래서 직접 required init(coder:)를 구현해줘도 괜찮지만, 새로운 init을 작성할 때 convenience init으로 생성을 해주면 init의 옵션(?)을 단순히 추가해주는 것이라 required init(coder:)을 따로 작성하지 않아도 된다!

Pagination 개선

어제 새롭게 알게 된 collectionView / tableView(_:willDisplay:forItemAt:) 메서드를 사용해서 Pagination의 로직을 개선했다. (이는 UICollectionViewDelegate, UITableViewDelegate에 있는 메서드이다)

어제 contentOffset에 대한 개념이 불명확했고, 셀의 높이가 달라지게 되면 페이지를 로드하는 순간도 달라져서 scrollViewDidScroll 메서드의 로직 개선이 필요하다고 판단했다.

extension MainViewController: UICollectionViewDelegate {
    func collectionView(
        _ collectionView: UICollectionView,
        willDisplay cell: UICollectionViewCell,
        forItemAt indexPath: IndexPath
    ) {
        if indexPath.item == products.count - 1,
           pageInformation?.hasNext == true,
           let num = pageInformation?.pageNumber {
            loadProductsList(pageNumber: num + 1)
        }
    }
}

따라서 현재 로딩된 product의 갯수와 indexPath.item(row)의 갯수를 확인해서 일치하는 경우만 페이지 number를 올리고 로드될 수 있도록 구현을 했다.

항상 동일한 간격으로 해당 메서드가 호출되는 만큼 이전보다 좋은 로직인 것 같다. 이해하기도 쉽고 말이다.

Comments