๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐ŸŽ iOS & Swift

[iOS] UIKit์—์„œ Preview ์‚ฌ์šฉํ•˜๊ธฐ

๋“ค์–ด๊ฐ€๊ธฐ์ „์—

๋งค๋ฒˆ ์กฐ๊ธˆ์˜ UI ์ˆ˜์ •์„ ํ•˜๋Š๋ผ ๋ช‡ ๋ฒˆ์˜ ๋นŒ๋“œ๋ฅผ ํ•˜์‹œ๋‚˜์š”? ๋ช‡ ๋ฒˆ์˜ CMD+R ํ‚ค๋ฅผ ๋ˆ„๋ฅด์‹œ์ฃ ?

์ €๋Š” ํ•˜๋‚˜์˜ ํ”„๋กœ์ ํŠธ๋ฅผ ํ•˜๋Š”๋ฐ๋„ ์ •๋ง ๋งŽ์ด ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ๋ฅผ ๋Œ๋ ค๋ณด๋Š”๋ฐ์š”.

๋ฐ”๋กœ๋ฐ”๋กœ ๋ณ€ํ™”๋ฅผ ๋ณผ ์ˆ˜๊ฐ€ ์—†์œผ๋‹ˆ๊นŒ ๋ถˆํŽธํ•˜๋”๋ผ๊ณ ์š”...

๊ทผ๋ฐ ๊ทธ๊ฑฐ ์•„์‹œ๋‚˜์š”? SwiftUI๋Š” ์ฝ”๋“œ๋ฅผ ์งœ๋ฉด์„œ ๋ฐ”๋กœ ์˜†์— ํ”„๋ฆฌ๋ทฐ๋ฅผ ๋ณผ ์ˆ˜๊ฐ€ ์žˆ์ฃ !

์ด๊ฑธ UIKit์—๋„ ์ ์šฉ์‹œ์ผœ๋ณด๋Š”๊ฒ๋‹ˆ๋‹ค.

์›๋ž˜ ๋ถˆํŽธํ•œ ๊ฑธ ํ•ด๊ฒฐํ•˜๋ฉด์„œ ๋ฐœ์ „ํ•˜๋Š”๊ฑฐ์ฃ ~๐Ÿ˜

ํ‰์†Œ์— ์ฝ”๋“œ๋กœ ui ๊ฐœ๋ฐœ์„ ์ฆ๊ฒผ๋‹ค๋ฉด ํ›จ์”ฌ ์œ ์šฉํ•œ ๋ฐฉ๋ฒ•์ผ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค :)


๐ŸŽ Preview๋ฅผ ์กฐ๊ธˆ ๋” ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ Extensions

โ†’ ํ•„์š”ํ•  ๋•Œ toPreview() ๋ฉ”์„œ๋“œ๋งŒ ํ˜ธ์ถœํ•ด์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

// UIViewControllerRepresentable extension
//
//  UIViewController+Preview.swift
//  PreviewPractice
//
//  Created by taehy.k on 2021/05/27.
//

import UIKit

#if DEBUG
import SwiftUI

@available(iOS 13, *)
extension UIViewController {
    private struct Preview: UIViewControllerRepresentable {
        // this variable is used for injecting the current view controller
        let viewController: UIViewController

        func makeUIViewController(context: Context) -> UIViewController {
            return viewController
        }

        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        }
    }

    func toPreview() -> some View {
        // inject self (the current view controller) for the preview
        Preview(viewController: self)
    }
}
#endif

๋” ๋‚˜์€ Extensions!

  • UIView๋‚˜ ์…€ ๋‹จ์œ„ ์ž‘์—…ํ• ๋•Œ๋Š” ์•„๋ž˜์— ์žˆ๋Š”๊ฒŒ ๋” ๋‚˜์•„๋ณด์ธ๋‹ค..!
//
//  UIViewPreview.swift
//  PreviewPractice
//
//  Created by taehy.k on 2021/05/27.
//

#if canImport(SwiftUI) && DEBUG
    import SwiftUI

    public struct UIViewPreview<View: UIView>: UIViewRepresentable {
        public let view: View
        public init(_ builder: @escaping () -> View) {
            view = builder()
        }
        // MARK: - UIViewRepresentable
        public func makeUIView(context: Context) -> UIView {
            return view
        }
        public func updateUIView(_ view: UIView, context: Context) {
            view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
            view.setContentHuggingPriority(.defaultHigh, for: .vertical)
        }
    }

    public struct UIViewControllerPreview<ViewController: UIViewController>: UIViewControllerRepresentable {
        public let viewController: ViewController

        public init(_ builder: @escaping () -> ViewController) {
            viewController = builder()
        }

        // MARK: - UIViewControllerRepresentable
        public func makeUIViewController(context: Context) -> ViewController {
            viewController
        }

        @available(iOS 13.0, tvOS 13.0, *)
        @available(OSX, unavailable)
        @available(watchOS, unavailable)
        public func updateUIViewController(_ uiViewController: ViewController, context: UIViewControllerRepresentableContext<UIViewControllerPreview<ViewController>>) {
            return
        }
    }
#endif

์‚ฌ์šฉ๋ฐฉ๋ฒ•

  • ๋ณ€ํ™”๋ฅผ ํ™•์ธํ•˜๊ณ  ์‹ถ์€ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ํ•˜๋‹จ ๋ถ€์— ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์ ์–ด์ฃผ๊ณ  ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
#if DEBUG
import SwiftUI

@available(iOS 13.0, *)
struct VCPreview: PreviewProvider {
        // Device ๋ฐฐ์—ด๋กœ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋””๋ฐ”์ด์Šค์— ์ ์šฉ๋œ ๋ชจ์Šต์„ ๊ฐ™์ด ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
        // ์ €๋Š” ์ง€๊ธˆ 3๊ฐ€์ง€์˜ Device๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์ฃ .
    static var devices = ["iPhone SE", "iPhone 11 Pro Max", "iPhone 12"]

    static var previews: some View {
        ForEach(devices, id: \.self) { deviceName in
            UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "ViewController")
                                // ์ต์Šคํ…์…˜์—์„œ ๋งŒ๋“  toPreview() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์ฃ !
                .toPreview()
                .previewDevice(PreviewDevice(rawValue: deviceName))
                .previewDisplayName(deviceName)
        }
    }
}
#endif
//
//  BorderedButton.swift
//  PreviewPractice
//
//  Created by taehy.k on 2021/05/27.
//

import UIKit

class MyBaseButton: UIButton {

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupView()
    }

    func setupView() {
        layer.cornerRadius = 4
        clipsToBounds = true
    }
}

#if DEBUG
import SwiftUI

@available(iOS 13.0, *)
struct BorderedButton_Preview: PreviewProvider {
    static var previews: some View {
                // ์ด๋Ÿฐ์‹์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹คโ€ผ๏ธ
        UIViewPreview {
            let button = MyBaseButton(frame: .zero)
            button.setTitle("Follow", for: .normal)
            button.setTitleColor(.blue, for: .normal)
            return button

        }
        .previewLayout(.sizeThatFits)
        .padding(10)
    }
}
#endif

Device rawValue

  • ์ง€์›ํ•˜๋Š” rawValue ์ž…๋‹ˆ๋‹ค.
  • ์•„๊นŒ ๋งŒ๋“ค์–ด๋‘์—ˆ๋˜ Device ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ•ด์„œ ์‚ฌ์šฉํ•˜์„ธ์š”!
// The following values are supported:
//
//     "iPhone 8"
//     "iPhone 8 Plus"
//     "iPhone SE"
//     "iPhone 11"
//     "iPhone 11 Pro"
//     "iPhone 11 Pro Max"
//     "iPad mini 4"
//     "iPad Air 2"
//     "iPad Pro (9.7-inch)"
//     "iPad Pro (12.9-inch)"
//     "iPad (5th generation)"
//     "iPad Pro (12.9-inch) (2nd generation)"
//     "iPad Pro (10.5-inch)"
//     "iPad (6th generation)"
//     "iPad Pro (11-inch)"
//     "iPad Pro (12.9-inch) (3rd generation)"
//     "iPad mini (5th generation)"
//     "iPad Air (3rd generation)"
//     "Apple TV"
//     "Apple TV 4K"
//     "Apple TV 4K (at 1080p)"
//     "Apple Watch Series 2 - 38mm"
//     "Apple Watch Series 2 - 42mm"
//     "Apple Watch Series 3 - 38mm"
//     "Apple Watch Series 3 - 42mm"
//     "Apple Watch Series 4 - 40mm"
//     "Apple Watch Series 4 - 44mm"
  • ์ „์ฒด์ฝ”๋“œ๊ฐ€ ๊ถ๊ธˆํ•˜๋ฉด ์•„๋ž˜๋ฅผ ํŽผ์ณ์„œ ํ™•์ธํ•˜์„ธ์š”.
    • ์ „์ฒด์ฝ”๋“œ

์‚ฌ์šฉ๋ชจ์Šต (๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ, ํ…Œ์ด๋ธ”์…€)

[Preview ์‚ฌ์šฉ๋ชจ์Šต 1] ํ”„๋ฆฌ๋ทฐ๋กœ ์‹ค์‹œ๊ฐ„ ๋ณ€ํ™”ํ•˜๋Š” ๋ชจ์Šต์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

[Preview ์‚ฌ์šฉ๋ชจ์Šต 2] ์…€์„ ๋งŒ๋“ ๋‹ค๋ฉด ์ด๋Ÿฐ์‹์œผ๋กœ Preview๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๊ฒ ์ฃ ?

Cell(์…€) ์ƒํƒœ์— ๋Œ€ํ•ด์„œ ์ฝ”๋“œ๋ฅผ ์ ์šฉํ•˜๊ณ  ์‹ถ์œผ๋ฉด

// XIB๋กœ ๋”ฐ๋กœ ๋นผ์„œ ์…€์„ ์ œ์ž‘ํ•œ๋‹ค๋ฉด ๋ฐ˜๋“œ์‹œ ์ด๋Ÿฐ์‹์œผ๋กœ ์ ‘๊ทผํ•ด์ค˜์•ผ ํ™”๋ฉด์— ์ž˜ ๋‚˜์˜ต๋‹ˆ๋‹ค.

UINib(nibName: "InstagramDMTVC", bundle: nil)
    .instantiate(withOwner: nil, options: nil).first as! InstagramDMTVC
#if DEBUG
import SwiftUI

struct InstagramDMTVCRepresentable: UIViewRepresentable {
    typealias UIViewType = InstagramDMTVC

    func makeUIView(context: Context) -> InstagramDMTVC {
        return UINib(nibName: "InstagramDMTVC", bundle: nil)
                            .instantiate(withOwner: nil, options: nil).first as! InstagramDMTVC
    }

    func updateUIView(_ uiView: InstagramDMTVC, context: Context) {
        uiView.setData(imageName: "profile",
                       name: "test_account",
                       message: "Sounds good ๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚",
                       time: ".now")
    }
}

@available(iOS 13.0, *)
struct InstagramDMTVCPreview: PreviewProvider {
    static var previews: some View {
        Group {
            InstagramDMTVCRepresentable()
                .frame(width: 375, height: 72)
            InstagramDMTVCRepresentable()
                .frame(width: 320, height: 100)
            InstagramDMTVCRepresentable()
                .frame(width: 320, height: 100)
        }
        .previewLayout(.sizeThatFits)
        .padding(10)
    }
}
#endif

์ฐธ๊ณ ์ž๋ฃŒ

[4์›” ์šฐ์•„ํ•œํ…Œํฌ์„ธ๋ฏธ๋‚˜] ๋งŒํ™”๊ฒฝ ์•ฑ ๊ฐœ๋ฐœ๊ธฐ

Use Xcode Previews with UIKit

SwiftUI ์—†์ด Xcode Preview ์‚ฌ์šฉํ•˜๊ธฐ

https://gist.github.com/Koze/d7ad0b172794fd3b55fb76ebc6f03051