Computer >> 컴퓨터 >  >> 스마트폰 >> iPhone

AutoLayout으로 iOS Spotify 클론 코딩하기: 이미지 추가 및 UI 업데이트

시작하며

이 글은 AutoLayout을 활용해 프로그래밍 방식으로 Spotify UI 클론을 구축하는 시리즈의 두 번째 파트입니다. 혹시 첫 번째 파트를 아직 보지 못하셨다면 걱정하지 마세요. 지금 바로 이전 글을 확인해 보시길 권합니다.

이번 시간에는 목업(mock) 이미지를 추가하고, UI를 실제 Spotify와 최대한 비슷하게 다듬어 보겠습니다.

오늘 완성할 결과물은 다음과 같습니다.

AutoLayout으로 iOS Spotify 클론 코딩하기: 이미지 추가 및 UI 업데이트

그리고 첫 번째 파트에서 우리가 멈춰 있던 지점은 바로 여기입니다.

AutoLayout으로 iOS Spotify 클론 코딩하기: 이미지 추가 및 UI 업데이트

커스텀 셀(Custom Cell) 만들기

다음 단계는 커스텀 셀을 만드는 것입니다. SubCustomCell이라는 이름의 셀부터 시작해 보겠습니다.

먼저 프로젝트 폴더 안에 새로운 Swift 파일을 생성하고 SubCustomCell.swift라고 이름을 붙입니다. 이 파일에는 플레이리스트를 나타낼 커스텀 셀이 담깁니다. 파일을 생성한 뒤 아래 코드를 추가하고, 예를 들어 backgroundColor로 셀을 초기화해 보세요. 그러면 collectionView에 셀을 등록했을 때 UI가 어떻게 변하는지 눈으로 확인할 수 있습니다.

import UIKit

class SubCustomCell: UICollectionViewCell {
        override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .red
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

이어서 CustomCell.swift 내부의 init 블록 안에서 SubCustomCell을 등록합니다. UICollectionViewCell.self를 아래와 같이 SubCustomCell로 교체하세요.

 collectionView.register(SubCustomCell.self, forCellWithReuseIdentifier: cellId)

또한 cellForItemAt 메서드도 수정해서 아래처럼 SubCustomCell 타입에 맞게 캐스팅해 주어야 합니다.

 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! SubCustomCell
        // cell.backgroundColor = .yellow
        
        return cell
    }

이제 실행하면 배경색이 red로 변경된 것을 확인할 수 있습니다.

AutoLayout으로 iOS Spotify 클론 코딩하기: 이미지 추가 및 UI 업데이트
Swift CustomCell

여기까지의 과정은 어렵지 않고 명확할 것입니다.

셀에 이미지 넣기

이제 각 셀 안에 ImageView를 만들어 목업 이미지로 채워 보겠습니다. 저는 pexels.com에서 무작위 이미지 몇 장을 미리 받아 두었는데, 여러분은 원하는 어떤 이미지를 사용해도 좋습니다(제가 사용한 이미지 포함). 해당 이미지들은 GitHub의 프로젝트 파일에서 찾을 수 있습니다.

SubCustomCell.swift 안에 UIImageView를 만들고 몇 가지 제약 조건(constraints)을 설정해 보겠습니다.

    let ImageView : UIImageView = {
       let iv = UIImageView()
        iv.backgroundColor = .yellow
        return iv
        
    }()

그리고 init 블록 안에서 addSubview를 사용해 이 요소를 뷰에 추가합니다.

 override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(ImageView)
            
    }

이제 아래 제약 조건을 통해 ImageView가 셀 안의 모든 공간을 차지하도록 만들어 보겠습니다.

 ImageView.translatesAutoresizingMaskIntoConstraints = false
            ImageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
            ImageView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
            ImageView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
            ImageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
  • leftAnchor: 셀의 왼쪽 앵커를 의미합니다.
  • rightAnchor: 셀의 오른쪽 앵커를 의미합니다.
  • bottomAnchor: 셀의 하단 앵커를 의미합니다.
  • topAnchor: 셀의 상단 앵커를 의미합니다.

ImageView의 top 앵커를 셀의 top 앵커와 일치시키고(left, right, bottom도 동일하게), 이렇게 하면 ImageViewSubCustomCell(셀) 전체 공간을 꽉 채우게 됩니다.

참고: 먼저 translatesAutoresizingMaskIntoConstraints를 사용해야 요소에 제약 조건을 적용할 수 있습니다. 또한 isActive 속성을 반드시 호출하여 true로 지정해야 한다는 점도 잊지 마세요. 이 과정을 생략하면 제약 조건이 동작하지 않아 화면에 아무 변화도 나타나지 않습니다.

ImageView에는 당연히 이미지가 들어가야 하니, 하나 추가해 보겠습니다.

 let ImageView : UIImageView = {
       let iv = UIImageView()
        iv.backgroundColor = .yellow
        // 프로젝트 안에 >image1< 파일이 있습니다.
        iv.image = UIImage(named: "image1")
        iv.contentMode = .scaleAspectFill
        iv.clipsToBounds = true
      
        return iv
        
    }()

앱을 빌드하고 실행하면 SubCustomCell에 추가한 이미지와 함께 결과를 확인할 수 있습니다.

AutoLayout으로 iOS Spotify 클론 코딩하기: 이미지 추가 및 UI 업데이트

좋습니다. 이제 SubCustomCell을 완성하기 위해 하나의 요소를 더 추가해야 합니다. 바로 플레이리스트의 제목을 나타낼 UILabel입니다.

타이틀은 다음과 같이 구성합니다.

 let TitleLabel : UILabel = {
        let lb = UILabel()
        lb.textColor = UIColor.lightGray
        lb.font = UIFont.systemFont(ofSize: 16)
        lb.font = UIFont.boldSystemFont(ofSize: 20)
        lb.text = "Evening Music"
     
        return lb
    }()

일단 임의의 텍스트를 넣어 두었지만, 원하는 문구로 자유롭게 변경하셔도 됩니다. 다음 단계는 이 요소를 뷰에 추가하고 제약 조건을 부여하는 것입니다. 타이틀은 ImageView 하단에 위치하게 됩니다.

뷰에 추가하기:

addSubview(TitleLabel)

ImageViewTitleLabel에 제약 조건 적용하기

 ImageView.translatesAutoresizingMaskIntoConstraints = false
            ImageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
            ImageView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
            ImageView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
            ImageView.heightAnchor.constraint(equalToConstant: 240).isActive = true
            ImageView.bottomAnchor.constraint(equalTo: TitleLabel.topAnchor).isActive = true
            
           
           
            TitleLabel.translatesAutoresizingMaskIntoConstraints = false
            TitleLabel.topAnchor.constraint(equalTo: ImageView.bottomAnchor,constant: 10).isActive = true
            TitleLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 5).isActive = true
            TitleLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -5).isActive = true

자, 완성되었습니다!

AutoLayout으로 iOS Spotify 클론 코딩하기: 이미지 추가 및 UI 업데이트

이미지가 셀 안 대부분의 공간을 차지하고, 남은 공간은 타이틀이 차지하도록 만들었습니다. 화면에서 볼 수 있듯이 각 섹션에서는 가로 스크롤이, 전체 화면에서는 세로 스크롤이 가능합니다.

AutoLayout으로 iOS Spotify 클론 코딩하기: 이미지 추가 및 UI 업데이트

JSON 목업 데이터 연동하기

이제 셀에 목업 데이터를 넣어서 실제 앱처럼 느껴지게 만들어 보겠습니다. 이를 위해 저는 섹션과 플레이리스트에 대한 무작위 데이터를 담은 JSON 파일을 하나 만들었습니다.

먼저 SectionPlaylist라는 두 개의 구조체(struct)를 생성합니다. 각 구조체는 별도의 파일로 분리해서 만듭니다.

section.swift

import Foundation
struct Section {
    var title : String
    var playlists : NSArray
    init(dictionary:[String : Any]) {
        self.title = dictionary["title"] as? String ?? ""
        self.playlists = dictionary["playlists"] as? NSArray ?? []
        
}
}

playlist.swift

//
//  playlist.swift
//  spotifyAutoLayout
//
//  Created by admin on 12/6/19.
//  Copyright © 2019 Said Hayani. All rights reserved.
//

import Foundation
struct PlayList {
    var title: String
    var image : String
    init(dictionary : [String : Any]) {
        self.title = dictionary["title"] as? String ?? ""
        self.image = dictionary["image"] as? String ?? ""
    }
   
}

그다음 ViewController.swift 안에서 JSON을 가져와(fetch) 그 결과를 배열에 저장하는 함수를 작성합니다.


        print("attempt to fetch Json")
        if let path = Bundle.main.path(forResource: "test", ofType: "json") {
            do {
                  let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
                  let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
                if let jsonResult = jsonResult as? [ Any] {
                            // do stuff
                    jsonResult.forEach { (item) in
                      
                        let section = Section(dictionary: item as! [String : Any])
                       // print("FEtching",section.playlists)
                        self.sections.append(section)
                    }
                    
                 
                  self.collectionView.reloadData()
                  }
              } catch {
                   // handle error
              }
        }
    }

fetchJson 함수는 ViewDidLoad 메서드 안에서 호출됩니다. 또한 결과를 저장할 sections라는 변수도 준비되어 있습니다.

 var sections = [Section]()

ViewController에서 CustomCell로 데이터 전달하기

다음 단계는 ViewController에서 CustomCell로 데이터를 전달하는 것입니다. 이를 위해 CustomCell 내부에 각 섹션의 데이터를 받을 변수를 생성합니다.

 var section : Section?{
        didSet{
            print("section ✅",self.section)
        }
    }

ViewControllercellForItemAt 메서드를 사용해 데이터를 CustomCell에 직접 전달합니다.

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CustomCell
         
        cell.section = sections[indexPath.item]
         
        return cell
    }

참고: fetchJson이 호출될 때마다 항상 self.collectionView.reloadData()를 호출하기 때문에, CustomCell 내부의 아래 블록 역시 함께 호출됩니다. 콘솔(shift + command + C)에서 확인해 보세요.

 var section : Section? {
        didSet{
            print("section ✅",self.section)
        }
    }

가장 먼저 할 일은 섹션 타이틀을 설정하는 것입니다.

 var section : Section? {
        didSet{
            print("section ✅",self.section)
            guard let section = self.section else {return}
            self.titleLabel.text = section.title
        }
    }

이제 화면에서 각 섹션이 고유한 제목을 가지고 있는 것을 확인할 수 있습니다.

AutoLayout으로 iOS Spotify 클론 코딩하기: 이미지 추가 및 UI 업데이트

SubCustomCell로 데이터 전달하기

이제 데이터를 SubCustomCell까지 전달할 차례입니다. 위에서 했던 것과 동일한 방식으로 진행합니다. playlists 배열을 전달해야 하므로, CustomCell 안에 playlists라는 이름의 변수를 생성합니다.

 var playlists : [PlayList]() //empty 

먼저 JSON의 playlists를 순회(map)한 뒤, 각 플레이리스트를 playlists 변수에 추가합니다.

 var section : Section? {
        didSet{
            print("section ✅",self.section)
            guard let section = self.section else {return}
            self.titleLabel.text = section.title
            // append to playlists array
             self.section?.playlists.forEach({ (item) in
                let playlist = PlayList(dictionary: item as! [String : Any])
                self.playlists.append(playlist)

            })
            self.collectionView.reloadData()
        }
    }

주의! 여기서 앱을 실행하면 크래시(crash)가 발생할 수 있습니다. 섹션의 개수를 설정하는 것을 잊었기 때문입니다. 이제 JSON에서 데이터를 받아오고 있으므로, 개수는 보유한 섹션 수에 따라 동적으로 결정되어야 합니다. 섹션의 개수는 JSON 내부의 섹션 개수와 같아야 하므로, ViewControllernumberOfItemsInSection을 아래와 같이 수정해야 합니다.

   override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return sections.count
    }

CustomCell.swift 안의 같은 메서드에도 동일한 작업을 해줍니다. 다만 여기서는 playlists의 개수를 기준으로 삼습니다.

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return  self.playlists.count
    }

마지막으로 완료해야 할 단계는 CustomCell.swiftcellForItemAt에서 각각의 플레이리스트 ObjectSubCustomCell에 전달하는 것입니다.

 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! SubCustomCell
        // here ?
        cell.playlist = playlists[indexPath.item]
        return cell
    }

그리고 SubCustomCell에서는 playlist 변수를 통해 해당 데이터를 받아, 최종적으로 플레이리스트의 제목과 이미지를 화면에 표시합니다.

var playlist : PlayList? {
           didSet{
               print("Playlist ?",self.playlist)
            guard let playlist = self.playlist else {return}
            // The Image ?
            self.ImageView.image = UIImage(named: playlist.image)
            // the playlist title ?
            self.TitleLabel.text = self.playlist?.title
               
           }
       }

이제 모든 것이 아래 영상처럼 정상적으로 동작할 것입니다.

AutoLayout으로 iOS Spotify 클론 코딩하기: 이미지 추가 및 UI 업데이트

UI 마무리 다듬기

마지막 UI 업데이트입니다. sectionplaylist 타이틀에 패딩(padding)과 마진(margin)을 추가하고, 플레이리스트 크기를 조금 더 작게 만들어야 합니다.

먼저 섹션 타이틀에 패딩을 추가해 보겠습니다. 이를 위해서는 섹션 셀인 CustomCellsetupSubCells 안에서 constant 속성에 숫자 값을 지정해 주기만 하면 됩니다.

 collectionView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor,constant: 15).isActive = true

collectionView 전체가 titleLabel 하단에 딱 붙어 있다면, 15를 추가해 여유 공간을 더 확보해 주면 됩니다.

AutoLayout으로 iOS Spotify 클론 코딩하기: 이미지 추가 및 UI 업데이트

다음은 playlist의 타이틀 차례입니다. 이 부분은 SubCustomCell 안에 있으며, ImageView 하단에 여유 공간을 조금 더 추가해 주기만 하면 됩니다.

 ImageView.bottomAnchor.constraint(equalTo: TitleLabel.topAnchor,constant: -15).isActive = true

이미 constant 값이 존재하므로, 정상적으로 동작하려면 값이 -15여야 합니다.

AutoLayout으로 iOS Spotify 클론 코딩하기: 이미지 추가 및 UI 업데이트

마지막으로 플레이리스트를 조금 더 작게 만들어야 합니다. 이것은 간단합니다. playlist 셀의 높이와 너비를 section 셀 높이를 2로 나눈 값과 같게 만들어 주면 됩니다. 아래를 참고하세요.

CustomCell.swift

 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        
        let width = frame.height / 2
        let height = frame.height / 2
        
        return CGSize(width: width, height: height)
        
    }

ImageView의 높이도 150으로 맞춰 줍니다.

  //SubCutomCell.swift
  ImageView.heightAnchor.constraint(equalToConstant: 150).isActive = true

자, 완성되었습니다!

AutoLayout으로 iOS Spotify 클론 코딩하기: 이미지 추가 및 UI 업데이트

마치며

완벽합니다! 오늘은 여기까지가 좋을 것 같습니다. 이 글이 너무 길어지지 않았으면 하거든요. 다음 파트에서는 TabBar와 설명 영역, 그리고 플레이리스트용 아이콘들을 추가해 보겠습니다.

GitHub에서 전체 소스 코드를 확인해 보세요.

시간 내어 읽어 주셔서 감사합니다. 빠진 내용이 없기를 바랍니다. 혹시 있다면 트위터로 @멘션 부탁드리며, 이 글에 대한 질문이나 추가하고 싶은 내용이 있다면 언제든 환영합니다. 감사합니다.

이 튜토리얼의 세 번째 파트가 게시되면 알림을 받으실 수 있도록 제 이메일 리스트를 구독해 주세요.

참고로, 최근 제 모바일 애플리케이션 중 하나를 위해 실력 있는 소프트웨어 엔지니어 그룹과 함께 작업한 경험이 있습니다. 협업 과정이 훌륭했고, 제품이 매우 신속하게 전달되었습니다. 제가 그동안 함께해 온 다른 회사나 프리랜서보다 훨씬 빠른 속도였죠. 다른 프로젝트에도 솔직히 추천할 만하다고 생각합니다. 연락을 원하시면 이메일(said@devsdata.com)을 보내 주세요.