【Swift 5】UICollectionView也能變桌布?

這篇應該算應該也是個『極短篇』吧?主要是因為有看到這一篇文章介紹的APP - Grid Wallpaper,原本以為還滿好做的(其實是想省錢啦),不過實際上試了之後發現問題不是在程式好不好寫,而是不知道要怎麼去排版,因為沒有iPhone桌面上的相關尺寸資訊(還是我找不到呢?),所以是使用馬克鰻 - MarkMan去量的,沒錯,用量的,話不多說,讓我們看下去吧。

首先呢,做一個長這樣的APP

製作過程

UICollectionView

  • 首先,先讓UICollectionView會動,按下去有反應,這裡就不多做說明了。
// MARK: - UICollectionViewDelegate
extension ViewController: UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let cell = collectionView.cellForItem(at: indexPath)
        cell?.backgroundColor = gridColor
    }
}

// MARK: - UICollectionViewDataSource
extension ViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return iPhoneSizeInformation.iconMaxCount
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
        cell.backgroundColor = .white
        
        return cell
    }
}

// MARK: - UICollectionViewDelegateFlowLayout
extension ViewController: UICollectionViewDelegateFlowLayout {
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return itemSizeMaker(with: indexPath)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return .zero
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return CGFloat(integerLiteral: 0)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return CGFloat(integerLiteral: 0)
    }
}

iPhone的尺寸相關資訊

  • 這裡主要是參考iPhone Development 101上的資訊作一個整理,比較要注意的是,iPhone上的最大icon的數量,在4.7吋之後的是滿版28個。
enum iPhoneSizeInformation {
    
    case _35inch
    case _40inch
    case _47inch
    case _55inch
    case _58inch
    case _61inch
    case _65inch
    
    static var type: iPhoneSizeInformation? { return getType() }
    static var bounds: ScreenBoundsInfo { return getBounds() }
    static var iconMaxCount: Int { return pageCountInfos()[type ?? _35inch] ?? 0 }
    
    /// Screen的資訊
    struct ScreenBoundsInfo: Equatable {
        var width: CGFloat
        var height: CGFloat
        var scale: CGFloat
    }
    
    /// 取得iPhone的Screen的大小Type
    private static func getType() -> iPhoneSizeInformation? {
        
        let boundsInfo = ScreenBoundsInfo(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height, scale: UIScreen.main.scale)
        let screenBoundsInfos = mainScreenBoundsInfos()
        
        for _screenBoundsInfo in screenBoundsInfos {
            if (_screenBoundsInfo.value == boundsInfo) { return _screenBoundsInfo.key }
        }
        
        return nil
    }
    
    /// 取得iPhone的Screen的大小
    private static func getBounds() -> ScreenBoundsInfo {
        return ScreenBoundsInfo(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height, scale: UIScreen.main.scale)
    }
    
    /// 取得記錄iPhone的大小跟比例的Array (https://www.idev101.com/code/User_Interface/sizes.html)
    private static func mainScreenBoundsInfos() -> [iPhoneSizeInformation: ScreenBoundsInfo] {
        
        let screenBoundsInfos: [iPhoneSizeInformation: ScreenBoundsInfo] = [
            ._35inch: ScreenBoundsInfo(width: 320, height: 480, scale: 2),
            ._40inch: ScreenBoundsInfo(width: 320, height: 568, scale: 2),
            ._47inch: ScreenBoundsInfo(width: 375, height: 667, scale: 2),
            ._55inch: ScreenBoundsInfo(width: 414, height: 736, scale: 3),
            ._58inch: ScreenBoundsInfo(width: 375, height: 812, scale: 3),
            ._61inch: ScreenBoundsInfo(width: 375, height: 812, scale: 2),
            ._65inch: ScreenBoundsInfo(width: 414, height: 896, scale: 3),
        ]
        
        return screenBoundsInfos
    }
    
    /// 取得記錄iPhone的單頁icon最大數量Array
    private static func pageCountInfos() -> [iPhoneSizeInformation: Int] {
        
        let countInfos: [iPhoneSizeInformation: Int] = [
            ._35inch: 24,
            ._40inch: 24,
            ._47inch: 28,
            ._55inch: 28,
            ._58inch: 28,
            ._61inch: 28,
            ._65inch: 28,
        ]
        
        return countInfos
    }
}

存照片 / 改變顏色

class ViewController: UIViewController {

    /// 存照片
    @IBAction func saveWallpaper(_ sender: UIButton) {
        guard let image = captureImageMaker(with: view) else { fatalError() }
        UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
    }
    
    /// 改變顏色
    @IBAction func changeGridColor(_ sender: UIButton) {
        showGridColorSettingAlertController(with: sender)
    }
    
    /// 存完照片後的動作
    @objc func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
        if let error = error { showAlertController(with: "錯誤", message: error.description); return }
        showAlertController(with: "成功", message: "已經存到您的照片內了")
    }
}

畫面配置

  • 最重要的點來了,就是關於各iPhone畫面上的設定,這裡是使用馬克鰻 - MarkMan去量手機上的圖的,有圖有真相。比較要注意的是,iPhone的比例的問題(UIScreen.main.scale),各位看倌要自己換算一下喲。
// MARK: - 主工具
extension ViewController {
    
    /// 取得Screen上的距離數據 (用MarkMan量的)
    private func getScreenBoundsInfo(for iPhoneType: iPhoneSizeInformation) -> iPhoneDesktopInfo? {
        return getScreenBoundsInfoArray()[iPhoneType]
    }
    
    /// 取得Screen上第一列 / 最後一列的icon位置
    private func getIconRangeInfo(for iPhoneType: iPhoneSizeInformation) -> iPhoneIconRangeInfo? {
        return getIconRangeInfoArray()[iPhoneType]
    }
    
    /// 取得Screen上的距離數據Array (用MarkMan量的)
    private func getScreenBoundsInfoArray() -> [iPhoneSizeInformation: iPhoneDesktopInfo] {
        
        let screenBoundsInfos: [iPhoneSizeInformation: iPhoneDesktopInfo] = [
            ._35inch: iPhoneDesktopInfo(iconSize: CGSize(width: 60, height: 60), gapSize: CGSize(width: 16, height: 28), barDistance: (top: 27, bottom: 129)),
            ._40inch: iPhoneDesktopInfo(iconSize: CGSize(width: 60, height: 60), gapSize: CGSize(width: 16, height: 28), barDistance: (top: 27, bottom: 129)),
            ._47inch: iPhoneDesktopInfo(iconSize: CGSize(width: 60, height: 60), gapSize: CGSize(width: 27, height: 28), barDistance: (top: 30, bottom: 137)),
            ._55inch: iPhoneDesktopInfo(iconSize: CGSize(width: 60, height: 60), gapSize: CGSize(width: 34.75, height: 40), barDistance: (top: 38, bottom: 139)),
            ._58inch: iPhoneDesktopInfo(iconSize: CGSize(width: 60, height: 60), gapSize: CGSize(width: 27, height: 42), barDistance: (top: 72, bottom: 170)),
            ._61inch: iPhoneDesktopInfo(iconSize: CGSize(width: 60, height: 60), gapSize: CGSize(width: 27, height: 42), barDistance: (top: 72, bottom: 170)),
            ._65inch: iPhoneDesktopInfo(iconSize: CGSize(width: 64, height: 64), gapSize: CGSize(width: 31.6, height: 49), barDistance: (top: 78, bottom: 192)),
        ]
        
        return screenBoundsInfos
    }
    
    /// 取得Screen上第一列 / 最後一列的icon位置Array
    private func getIconRangeInfoArray() -> [iPhoneSizeInformation: iPhoneIconRangeInfo] {
        
        let screenBoundsInfos: [iPhoneSizeInformation: iPhoneIconRangeInfo] = [
            ._35inch: iPhoneIconRangeInfo(firstColumn: 0...3, lastColumn: 20...23),
            ._40inch: iPhoneIconRangeInfo(firstColumn: 0...3, lastColumn: 20...23),
            ._47inch: iPhoneIconRangeInfo(firstColumn: 0...3, lastColumn: 24...27),
            ._55inch: iPhoneIconRangeInfo(firstColumn: 0...3, lastColumn: 24...27),
            ._58inch: iPhoneIconRangeInfo(firstColumn: 0...3, lastColumn: 24...27),
            ._61inch: iPhoneIconRangeInfo(firstColumn: 0...3, lastColumn: 24...27),
            ._65inch: iPhoneIconRangeInfo(firstColumn: 0...3, lastColumn: 24...27),
        ]
        
        return screenBoundsInfos
    }
}

擷取畫面 / 尺寸配置

extension ViewController {
    
    /// 擷取UIView的畫面 => UIImage
    private func captureImageMaker(with view: UIView) -> UIImage? {
        
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
        buttonsHidden(true)

        defer {
            UIGraphicsEndImageContext()
            buttonsHidden(false)
        }
        
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        view.layer.render(in: context)
        let image = UIGraphicsGetImageFromCurrentImageContext()

        return image
    }
    
    /// 產生item對應的大小
    private func itemSizeMaker(with indexPath: IndexPath) -> CGSize {
        
        guard let iPhoneType = iPhoneSizeInformation.type,
              let desktopSizeInfo = getScreenBoundsInfo(for: iPhoneType),
              let iconRangeInfo = getIconRangeInfo(for: iPhoneType),
              let iconSize = Optional.some(desktopSizeInfo.iconSize),
              let gapSize = Optional.some(desktopSizeInfo.gapSize),
              let barDistance = Optional.some(desktopSizeInfo.barDistance),
              let marginTop: CGFloat = Optional.some(barDistance.top - gapSize.height * 0.5),
              let bottomHeight: CGFloat = Optional.some(barDistance.bottom - gapSize.height * 0.5),
              let rowType = RowType(rawValue: indexPath.row % 4)
        else {
            return .zero
        }
        
        var itemSize = CGSize(width: iconSize.width + gapSize.width, height: iconSize.height + gapSize.height)
        
        switch indexPath.row {
        case iconRangeInfo.firstColumn: itemSize.height += marginTop
        case iconRangeInfo.lastColumn: itemSize.height = bottomHeight
        default: break
        }
        
        if rowType == .first || rowType == .last {
            itemSize.width += gapSize.width * 0.5
        }
        
        return itemSize
    }
}

範例程式碼下載

後記

  • 本來還以為一天就可以做完了,但實作了之後才知道問題點在哪裡,果然自己的經驗還不足,要多多學習才是。不過也讓我知道了MarkMan這個UI工具,算是個不錯的體驗啊。