【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
}
}
存照片 / 改變顏色
- 這裡主要是利用UIImageWriteToSavedPhotosAlbum來存照片
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
}
}
擷取畫面 / 尺寸配置
- 這裡使用UIGraphicsBeginImageContextWithOptions來擷取畫面,至於尺寸的計算也不多做說明了。
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工具,算是個不錯的體驗啊。