-
Notifications
You must be signed in to change notification settings - Fork 3
/
UIViewPreviewProvider.swift
198 lines (157 loc) · 5.65 KB
/
UIViewPreviewProvider.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
//
// UIViewPreviewProvider.swift
// PreviewsCompatibility
//
// Created by Max on 11/5/19.
// Copyright © 2019 Max. All rights reserved.
//
import Foundation
#if canImport(SwiftUI) && DEBUG
import SwiftUI
// MARK: - UIViewPreviewProvider
/// Allows rendering UIViews in the Xcode preview canvas pane `(⌥ + ⌘ + return)` meant for `SwiftUI`
///
/// Create a type conforming to this protocol somewhere in your file, and provide it with views to display.
///
/// The `SwiftUI` preview canvas pane `(⌥ + ⌘ + return)` should then start rendering the views you provided and should update whenever you edit your code.
///
///
/// *Note:* You will also need to explicitly conform to `PreviewProvider`. This protocol doesn't directly inherit from `PreviewProvider` but it does provide all the methods needed for auto-conformance
///
///
/// **Example**
///
///```
///#if canImport(SwiftUI) && DEBUG
///
///import SwiftUI
///
///@available(iOS 13.0, *)
///struct MyView_Preview: PreviewProvider, UIViewPreviewProvider {
///
/// static let uiPreviews: [Preview] = {
/// let view1 = MyView()
/// view1.someProperty = "foo"
///
/// let view2 = MyView()
/// view2.someProperty = "bar"
///
/// return [view1, view2].map { Preview($0) }
/// }()
///}
///```
public protocol UIViewPreviewProvider {
/// Array of `Preview` to be displayed in the Xcode canvas pane `(⌥ + ⌘ + return)`
static var uiPreviews: [Preview] { get }
/// `ColorScheme` to be used in the Xcode canvas pane
@available(iOS 13.0, *)
static var colorSchemes: [ColorScheme] { get }
}
// MARK: UIViewPreviewProvider + PreviewProvider
@available(iOS 13, *)
extension UIViewPreviewProvider {
/// Default color schemes
public static var colorSchemes: [ColorScheme] {
[.light, .dark]
}
/// Calculates the size we apply to the view
private static func size(for preview: Preview) -> CGSize {
switch preview.size {
case .intrinsic:
return preview.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
case .fixedWidth(let width):
var compressedSize = UIView.layoutFittingCompressedSize
compressedSize.width = width
let previewSize = preview.view.systemLayoutSizeFitting(
compressedSize,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)
return previewSize
case .fixed(let fixedSize):
return fixedSize
}
}
/// Converts a Preview to a SwiftUI View
static func swiftUIView(from preview: Preview, scheme colorScheme: ColorScheme) -> some View {
let size = self.size(for: preview)
preview.view.widthAnchor.constraint(equalToConstant: size.width).isActive = true
preview.view.heightAnchor.constraint(equalToConstant: size.height).isActive = true
return preview
.previewLayout(
.fixed(
width: size.width,
height: size.height
)
)
.previewDisplayName("\(preview.displayName ?? "") [\(colorScheme)]")
.environment(\.colorScheme, colorScheme)
}
/// Allows us to automatically conform to `PreviewProvider`
public static var previews: some View {
let identifiablePreviews = uiPreviews.map {
IdentifiableBox($0)
}
let identifieableSchemes = colorSchemes.map {
IdentifiableBox($0)
}
return ForEach(identifiablePreviews) { preview in
ForEach(identifieableSchemes) { scheme in
swiftUIView(from: preview.item, scheme: scheme.item)
}
}
}
}
// MARK: Preview
/// Wraps a UIView to make it `UIViewRepresentable` and provides additional information for displaying an Xcode preview
public struct Preview {
// MARK: - Size
/// How views contained in a `Preview` are sized
public enum Size {
/// Applies a fixed width with the given amount. Height is intrinsic
case fixedWidth(CGFloat)
/// Fixed height and width
case fixed(CGSize)
/// Uses the size returned by .`systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)`
case intrinsic
// Most common iPhone width
public static let `default`: Size = .fixedWidth(375)
}
fileprivate let view: UIView
fileprivate let size: Size
fileprivate let displayName: String?
/// Initializes a `Preview`
/// - Parameters:
/// - view: `UIView` being previewed
/// - size: How the view should be sized. Defaults to `.fixedWidth(375)`
/// - displayName: Optional display name shown below the preview. Defaults to `nil`
public init(
_ view: UIView,
size: Size = .default,
displayName: String? = nil
) {
self.view = view
self.size = size
self.displayName = displayName
}
}
// MARK: Preview + UIViewRepresentable
@available(iOS 13, *)
extension Preview: UIViewRepresentable {
public func makeUIView(context: Context) -> UIView {
return view
}
public func updateUIView(_ view: UIView, context: Context) {}
}
// MARK: IdentifiableBox
/// Wraps an existing type and makes it `Identifiable`
///
/// We need this so we can do a `ForEach` call with an array of `Preview`
@available(iOS 13.0, *)
private class IdentifiableBox<T>: Identifiable {
public let item: T
public init(_ item: T) {
self.item = item
}
}
#endif