TIL(230217)

Youth·2023년 2월 17일
0

1. 싱글톤패턴(Manager사용할때 유의할점)

유튜브에서 클론코딩을 하거나 구글링을 하다가 프로젝트 파일을 다운로드 받아서 보면 MVC에서 Model에 해당하는 Manager관련한 코드를 많이 본다. 보통은 싱글톤으로 구현한다. 이유는 간단하다 싱글톤으로 구현해서 어떤 View에서나 접근이 가능하게 만들기 위해서 일거다.

final class CoreDataManager {
    
    // 싱글톤으로 만들기
    static let shared = CoreDataManager()
    private init() {}
    
    func fetch() {}
}

보통은 위와같이 선언을 한다 class로 매니저를 선언하고 타입속성으로 클래스의 인스턴스를 할당해준다
중요한건 생성자를 private으로 선언해준다는 부분인데 이렇게 하는 이유는 어떻게 생각하면 당연하다
싱글톤이라는게 "유일한 객체"를 만들겠다는거기때문에 여기저기서 객체를 생성하면 안되는거니까?
그래서 저렇게 어디서든 생성할수 없게 private으로 생성자를 만들어줘야한다

그리고 사용할때는 아무 View에서(보통은 ViewController에서 사용한다)아래와 같이 사용하면된다

CoreDataManager.shared.fetch()

2. Constant를 사용하는 방법

앱을 만들다보면 생각보다 String자체를 사용하는 경우가 많다. Url같은거? 그리고 숫자 자체를 사용하는 경우가 많다. 그냥 예시를 들면 이런식일거다.

tableView.rowHeight = 120

보통 코드리뷰를 받으면 이런식의 Int를 아무런 설명없이 쓰는걸 별로 좋아하진 않는거같다 물론 코드를 쓰는 나는 이게 대충 어떤느낌의 숫자인지는 알고 저렇게 rowHeight같이 자주쓰는 메서드에는 사람들이 아 저거 cell높이겠거니 하지만 문제는 이런거다

let collectionCellWidth = (120 - 15 * (4 - 1)) / 4

나름 코드를 짠사람은 저 숫자하나하나가 의미가 있을거다 그러니까 저렇게 수식에다가 숫자를 넣어놨겠지...
근데 문제는 처음본사람은 저숫자들이 뭘의미하는지 알방법이 없다. 해석을 해야한다 그리고 넘겨짚어야한다 운이좋아서 이해하면 다행이지만 몰라서 물어보면 코드를짠사람도 시원하게 답변을 못하는 경우가 많다

코드를 짠사람에게도 문제가 생긴다 저기서 4는 한줄에 몇개의 cell을 넣을래라는 의미인데 이 코드를 2틀만 안보고 다시보면 저 숫자들이 뭘의미하는지 모른다는거다 그래서 갑자기 디자이너가

cell의 갯수를 5개로 늘려주세요

라고 말하면 저 4 라는 숫자의 의미도 파악하고 4라고 쓴 모든 Int를 5로 바꿔줘야하는데 5로바꿔야하는 4 말고 다른 4를 5로바꾸면 아주아주 UI가 꼬이고 답이없어진다.

그래서 보통 constant라는 파일을 만들어서 숫자나 string(Url)을 관리해준다

// 데이터 영역에 저장 (열거형, 구조체 다 가능 / 전역 변수로도 선언 가능)
// 사용하게될 API 문자열 묶음
public enum MusicApi {
    static let requestUrl = "https://itunes.apple.com/search?"
    static let mediaParam = "media=music"
}

// 사용하게될 Cell 문자열 묶음
public struct Cell {
    static let musicCellIdentifier = "MusicCell"
    static let musicCollectionViewCellIdentifier = "MusicCollectionViewCell"
    private init() {}
}

// 컬렉션뷰 구성을 위한 설정
public struct CVCell {
    static let spacingWitdh: CGFloat = 1
    static let cellColumns: CGFloat = 3
    private init() {}
}

이런식으로 만들어주게 되면 아까 같이 숫자들로만 이루어진 코드가

let collectionCellWidth = (UIScreen.main.bounds.width - CVCell.spacingWitdh * (CVCell.cellColumns - 1)) / CVCell.cellColumns

이런식으로 깔끔해진다
1.우선 누가봐도 어떤 수식인지 알아보기 쉽고
2.한줄에 들어가는 cell갯수가 바뀌어도 그냥 CVCell에서 cellClumns에 숫자만 바꿔주면 해당 속성을 사용하는 모든부분이 바뀌게된다. 유지보수 측면에서도 편리해진다

여기에서 새로알게된 사실이 있는데
나는 보통 상수를 타입속성으로 만들어서 사용할때 구조체로 사용했다 근데 private init은 사용을 안했는데 저거 자체가 인스턴스를 생성해서 사용하는게 아니기때문에 private init이 맞는거같다
그리고 enum으로 선언하는 방식은 처음봤는데 이런경우엔 private init을 안해줘서 되서 편할거같다


3. UIsearchController와 resultController의 차이점

saerchBar를 사용해봐야지 사용해봐야지 하면서 사용해본적이 없었는데 오늘 공부하면서 여러가지를 알게되었다
searchBar에는 기본 서치바와 resultController두 가지가 존재한다

나눠보자면
1. 그냥 나는 검색결과를 원래보여주던 view에서 보여주고 싶어 -> 기본 서치바 사용
2. 나는 검색결과를 다른 view로 보여주고 싶어 -> resultController를 사용하면된다

우선 기본서치바는 delegate를 사용하면된다

// 서치바 생성
let searchController = UISearchController()

// 서치바의 델리게이트 설정
searchController.searchBar.delegate = self

resultController는 조금 다르다

// 서치바에 resultController를 설정해준다
let searchController = UISearchController(searchResultsController: UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "SearchResultViewController") as! SearchResultViewController)

// 델리게이트가 아니라 searchResultUpdater를 self로 지정해준다
searchController.searchResultsUpdater = self

// 프로토콜을 채택해서 원하는 액션을 취해준다
extension ViewController: UISearchResultsUpdating {
    // 유저가 글자를 입력하는 순간마다 호출되는 메서드 ===> 일반적으로 다른 화면을 보여줄때 구현
    func updateSearchResults(for searchController: UISearchController) {
        print("서치바에 입력되는 단어", searchController.searchBar.text ?? "")
        // 글자를 치는 순간에 다른 화면을 보여주고 싶다면 (컬렉션뷰를 보여줌)
        let vc = searchController.searchResultsController as! SearchResultViewController
        // 컬렉션뷰에 찾으려는 단어 전달
        vc.searchTerm = searchController.searchBar.text ?? ""
    }
}

예를들어서 기본은 tableView에서 화면을 보여주다가 검색을 하면 인스타그램처럼 collectionView로 보여주고 싶다면 resultController를 설정한 서치바를 사용해주면 된다


4. tableview에서 image를 네트워킹으로 받아오는 방법

이건 평소에 내가 간과했던 부분이었는데 공부를 하면서 알게된 부분이다
보통 urlsession으로 json을 받아오고 거기에 image정보가 있다면 보통은 image url인경우가 많다
그리고 그 image url로 이미지를 불러와서 imageView에 image로 넣어주는 방식인데
나같은 경우는 그 image를 불러오는걸 viewcontorller에서 아얘 이미지를 불러와서 cell에 이미지에 configuration해주는 방식으로 코드를 짜왔다

근데 그렇게 코드를 짜면 cell이 reuse되는 과정에서 스크롤이 빨라지면 문제가 발생하고 이부분이 유저의 사용경험에 부정적인 영향을 미칠 수도 있다는걸 알게되었다

그래서 정리해보자면
1.ViewController는 Cell에 Image가 아닌 ImageUrl을 단순히 넘겨준다
2.Cell에서 Image를 load해서 image를 띄워준다
3.Cell에서 이미지를 띄워줄때 reuse할때 이미지를 초기화 시켜주고 특정상황으로 인해서 이미지 로드가 되고있는동안에 url이 바뀔가능성을 제거해줌으로서 자연스러운 UX를 구성한다

우선 cell에서는 이런식으로 구성한다(전체코드아님)

final class MusicCollectionViewCell: UICollectionViewCell {
    
    @IBOutlet private weak var mainImageView: UIImageView!
    
    // 이미지 URL을 전달받는 속성
    var imageUrl: String? {
        didSet {
            loadImage()
        }
    }

ViewController에서 Cell에는 파싱한 데이터에서 ImageUrl을 넘겨준다

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        print(#function)
        return self.musicArrays.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = musicTableView.dequeueReusableCell(withIdentifier: Cell.musicCellIdentifier, for: indexPath) as! MusicCell
        

        //⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️
        cell.imageUrl = musicArrays[indexPath.row].imageUrl
        
        cell.songNameLabel.text = musicArrays[indexPath.row].songName
        cell.artistNameLabel.text = musicArrays[indexPath.row].artistName
        cell.albumNameLabel.text = musicArrays[indexPath.row].albumName
        cell.releaseDateLabel.text = musicArrays[indexPath.row].releaseDateString
        
        cell.selectionStyle = .none
        return cell
    }
}

그러면 Cell에서는 didSet으로 imageUrl이 들어오며되면 loadImage()라는 메서드를 실행한다

    private func loadImage() {
        guard let urlString = self.imageUrl, let url = URL(string: urlString)  else { return }
        
        // 오래걸리는 작업을 동시성 처리 (다른 쓰레드에서 일시킴)
        DispatchQueue.global().async {
            // URL을 가지고 데이터를 만드는 메서드 (오래걸리는데 동기적인 실행)
            // (일반적으로 이미지를 가져올때 많이 사용)
            guard let data = try? Data(contentsOf: url) else { return }
            // 오래걸리는 작업이 일어나고 있는 동안에 url이 바뀔 가능성 제거 ⭐️⭐️⭐️
            guard self.imageUrl! == url.absoluteString else { return }
            
            // 작업의 결과물을 이미로 표시 (메인큐)
            DispatchQueue.main.async {
                self.mainImageView.image = UIImage(data: data)
            }
        }
    }
    
    // 셀이 재사용되기 전에 호출되는 메서드
    override func prepareForReuse() {
        super.prepareForReuse()
        // 일반적으로 이미지가 바뀌는 것처럼 보이는 현상을 없애기 위해서 실행 ⭐️
        self.mainImageView.image = nil
    }

이미지 url에서 이미지를 생성하는데 contentOf를 사용해서 data를 만들고 data로 UIImage를 만들수있다는걸 처음 알게되었고
contentOf는 동시성으로 진행되서 dispatchQueue.global로 다른 쓰레드에 보내줘야한다는것도 알게되었고
cell이 reuse될때마다 image를 nil로 초기화해주는거 그리고 absoluteString이라는것도 처음알게 되었다

앱을 만들다보면 세세한 부분을 신경쓴앱과 아닌앱은 유저가 사용할때 티가 정말 많이 난다는걸 뼈저리게 느꼈기때문에 이부분을 잘 고려하는 개발자가 될 수 있도록 해야겠다

profile
AppleDeveloperAcademy@POSTECH 1기 수료, SOPT 32기 iOS파트 수료

0개의 댓글