├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md └── Sources ├── PreviewView ├── NavigationControllerPreview.swift ├── PreviewTabBarItem.swift ├── TabBarControllerPreview.swift ├── ViewControllerPreview.swift └── ViewPreview.swift └── PreviewViewLegacyOSCompileFix └── DesignTime+BackDeploy.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Joshua Asbury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "PreviewView", 7 | platforms: [ 8 | .iOS(.v11), 9 | ], 10 | products: [ 11 | .library(name: "PreviewView", targets: ["PreviewView"]), 12 | .library(name: "PreviewViewLegacyOSCompileFix", targets: ["PreviewViewLegacyOSCompileFix"]), 13 | ], 14 | targets: [ 15 | .target(name: "PreviewView"), 16 | .target(name: "PreviewViewLegacyOSCompileFix"), 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!TIP] 2 | > 3 | > The `#Preview` macro ([WWDC23 session](https://developer.apple.com/wwdc23/10252)) has replaced this library if you are using Xcode 15 or higher. 4 | > 5 | > If your Minimum Deployment target is set earlier than iOS 17 you will need to add an availability check. 6 | > 7 | > ```swift 8 | > @available(iOS 17, *) 9 | > #Preview { 10 | > YourViewController() 11 | > } 12 | > 13 | > @available(iOS 17, *) 14 | > #Preview { 15 | > YourView() 16 | > } 17 | > ``` 18 | 19 | # PreviewView 20 | 21 | Make use of SwiftUI previews for rapidly prototyping your `UIViewControllers` and `UIViews`! 22 | 23 | The SwiftUI preview canvas is tied to a specific version of Xcode, not the the target OS version. This means you can make use of this utility even if you're not targeting iOS 13 or higher, as long as you're using Xcode 10 or higher. 24 | 25 | ## My iOS deployment target is below iOS 13 26 | 27 | If you're targeting an iOS version earlier than iOS 13 then you may be get an error such as: 28 | ``` 29 | Compiling failed: '__designTimeString(_:fallback:)' is only available in iOS 13.0 or newer 30 | ``` 31 | Other known variants may be: 32 | - `__designTimeInteger(_:fallback:)` 33 | - `__designTimeBoolean(_:fallback:)` 34 | - `__designTimeFloat(_:fallback:)` 35 | 36 | To solve this issue this library provides another target, `PreviewViewLegacyOSCompileFix`, which adds functions annotated with `@backDeployed(before:)` so these functions exist on versions earlier than iOS 13. 37 | 38 | In addition the normal `import PreviewView` and `import SwiftUI` imports alongside your `PreviewProvider` you will need to add `import PreviewViewLegacyOSCompileFix` to ensure the back deployed functions are present. 39 | 40 | Don't forget to mark your `PreviewProvider` with `@available(iOS 13, *)`. 41 | 42 | ## Installation 43 | 44 | You can manually drop the files into your project, or take a look at Apple's documentation for [adding Swift Packages in Xcode](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app). 45 | 46 | Adding this as a dependency on a Swift Package is **not** recommended as it will then force the dependency on anyone that consumes your library. 47 | 48 | ## Previewing a view 49 | 50 | ```swift 51 | struct YourViewController_Previews: PreviewProvider { 52 | static var previews: some View { 53 | ViewPreview(YourView()) 54 | .previewLayout(.fixed(width: 375, height: 86)) 55 | } 56 | } 57 | ``` 58 | 59 | **Important:** Update the `previewLayout` values to be the typical size of your view. 60 | 61 | ## Previewing a view controller 62 | 63 | ### Standalone 64 | 65 | ```swift 66 | struct YourViewController_Previews: PreviewProvider { 67 | static var previews: some View { 68 | ViewControllerPreview(YourViewController()) 69 | .edgesIgnoringSafeArea(.all) 70 | } 71 | } 72 | ``` 73 | 74 | If you wish to test a custom `UINavigationController` you can do so with `ViewControllerPreview`. 75 | 76 | ```swift 77 | struct YourNavigationController_Previews: PreviewProvider { 78 | static var previews: some View { 79 | ViewControllerPreview(YourNavigationController()) 80 | .edgesIgnoringSafeArea(.all) 81 | } 82 | } 83 | ``` 84 | 85 | ### Embedded in a `UINavigationController` 86 | 87 | ```swift 88 | struct YourViewController_Previews: PreviewProvider { 89 | static var previews: some View { 90 | NavigationControllerPreview { 91 | YourViewController() 92 | } 93 | .edgesIgnoringSafeArea(.all) 94 | } 95 | } 96 | ``` 97 | 98 | The body content of the `NavigationControllerPreview` accepts an entire navigation stack, allowing your previews to show back bar button items, and even be navigatable in Live Preview. 99 | 100 | ```swift 101 | struct DetailViewController_Previews: PreviewProvider { 102 | static var previews: some View { 103 | NavigationControllerPreview { 104 | ListViewController() 105 | DetailViewController() 106 | } 107 | .edgesIgnoringSafeArea(.all) 108 | } 109 | } 110 | ``` 111 | 112 | You can customise the navigation bar settings and toolbar settings of the navigation controller via the initializer parameters. 113 | 114 | ```swift 115 | struct DetailViewController_Previews: PreviewProvider { 116 | static var previews: some View { 117 | NavigationControllerPreview(barStyle: .largeTitle, showsToolbar: true) { 118 | ListViewController() 119 | DetailViewController() 120 | } 121 | .edgesIgnoringSafeArea(.all) 122 | } 123 | } 124 | ``` 125 | 126 | ### Embedded in a `UITabBarController` 127 | 128 | ```swift 129 | struct YourViewController_Previews: PreviewProvider { 130 | static var previews: some View { 131 | TabBarControllerPreview { 132 | ViewControllerPreview(YourViewController()) 133 | } 134 | .edgesIgnoringSafeArea(.all) 135 | } 136 | } 137 | ``` 138 | 139 | Displaying a single tab would be weird, so to allow your previews to closely match your real app you can provide the other tabs within the body. 140 | 141 | ```swift 142 | struct YourViewController_Previews: PreviewProvider { 143 | static var previews: some View { 144 | TabBarControllerPreview { 145 | PreviewBlankTabItem(title: "First", image: UIImage(systemName: "capsule")) 146 | 147 | ViewControllerPreview(YourViewController()) 148 | 149 | ViewControllerPreview(YourOtherViewController()) 150 | } 151 | .edgesIgnoringSafeArea(.all) 152 | } 153 | } 154 | ``` 155 | 156 | You can even embed your view controllers in a navigation controller to get the full in-app experience. 157 | 158 | ```swift 159 | struct YourViewController_Previews: PreviewProvider { 160 | static var previews: some View { 161 | TabBarControllerPreview { 162 | PreviewBlankTabItem(title: "First", image: UIImage(systemName: "capsule")) 163 | 164 | NavigationControllerPreview { 165 | YourViewController() 166 | } 167 | 168 | PreviewBlankTabItem(title: "Third", image: UIImage(systemName: "diamond")) 169 | } 170 | .edgesIgnoringSafeArea(.all) 171 | } 172 | } 173 | ``` 174 | -------------------------------------------------------------------------------- /Sources/PreviewView/NavigationControllerPreview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationControllerPreview.swift 3 | // PreviewView 4 | // 5 | // Created by Josh Asbury on 7/8/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A custom parameter attribute that constructs a navigation stack from a closure. 11 | /// 12 | /// This is used by ``NavigationControllerPreview`` to provide a navigation stack. The last element in the navigation stack will be shown in the preview. 13 | @available(iOS 13, *) 14 | @resultBuilder 15 | public enum PreviewNavigationStackBuilder { 16 | public static func buildBlock(_ viewControllers: UIViewController...) -> [UIViewController] { viewControllers } 17 | } 18 | 19 | /// A type that can be used to preview in Xcode a `UIViewController` embedded within a `UINavigationController`. 20 | /// 21 | /// If you wish to preview a custom `UINavigationController` you can use ``ViewControllerPreview``. 22 | /// 23 | /// - SeeAlso: ``ViewControllerPreview`` 24 | /// - SeeAlso: ``TabBarControllerPreview`` 25 | @available(iOS 13, *) 26 | public struct NavigationControllerPreview: UIViewControllerRepresentable { 27 | /// A style for displaying the navigation bar of this navigation controller. 28 | public enum NavigationBarStyle { 29 | /// Hides the navigation bar. 30 | case none 31 | 32 | /// Defers to the active view controller's navigation item for how the navigation bar should behave. 33 | case `default` 34 | 35 | /// Display a large title within an expanded navigation bar. 36 | case largeTitle 37 | } 38 | 39 | /// The navigation controller that is being previewed. 40 | public let navigationController: UINavigationController 41 | 42 | /// Creates a navigation controller preview that displays a view controller with the given bar styles. 43 | /// 44 | /// - Parameters: 45 | /// - barStyle: The style to be applied to this navigation controller's `UINavigationBar`. The default value is ``NavigationBarStyle/default``. 46 | /// - showsToolbar: Whether the navigation controller should show its `UIToolbar`. The default value is `false`. 47 | /// - content: The view controllers of the navigation controller. 48 | /// - Returns: The initialized preview object. 49 | public init(barStyle: NavigationBarStyle = .default, showsToolbar: Bool = false, @PreviewNavigationStackBuilder _ content: () -> [UIViewController]) { 50 | let navigationController = UINavigationController() 51 | navigationController.viewControllers = content() 52 | switch barStyle { 53 | case .default: break 54 | case .none: navigationController.isNavigationBarHidden = true 55 | case .largeTitle: navigationController.navigationBar.prefersLargeTitles = true 56 | } 57 | navigationController.isToolbarHidden = !showsToolbar 58 | self.navigationController = navigationController 59 | } 60 | 61 | public func makeUIViewController(context: Context) -> some UIViewController { navigationController } 62 | public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} 63 | } 64 | -------------------------------------------------------------------------------- /Sources/PreviewView/PreviewTabBarItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewTabBarItem.swift 3 | // PreviewView 4 | // 5 | // Created by Josh Asbury on 7/8/21. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Represents an item that can be used by the ``TabBarControllerPreview``. 11 | @available(iOS 13, *) 12 | public protocol PreviewTabBarItem { 13 | /// Provides an instance of a view controller for use in the `UITabBarController`. 14 | var viewController: UIViewController { get } 15 | } 16 | 17 | @available(iOS 13, *) 18 | extension ViewControllerPreview: PreviewTabBarItem {} 19 | 20 | @available(iOS 13, *) 21 | extension NavigationControllerPreview: PreviewTabBarItem { 22 | public var viewController: UIViewController { navigationController } 23 | } 24 | 25 | /// A lightweight placeholder for a non-active view controller that exists within a tab bar. 26 | /// 27 | /// This can be used to acheive a preview that more closely represents the appearance in-app. 28 | /// 29 | /// ```swift 30 | /// TabBarControllerPreview { 31 | /// PreviewBlankTabItem(title: "Dashboard", symbolNamed: "rectangle.3.offgrid") 32 | /// } 33 | /// ``` 34 | /// - Important: This tab item may not be visible when your canvas is in Live Preview mode. 35 | @available(iOS 13, *) 36 | public struct PreviewBlankTabItem: PreviewTabBarItem { 37 | /// The view controller that backs this placeholder in the tab bar controller preview. 38 | public let viewController: UIViewController 39 | 40 | /// Creates a placeholder preview tab item with a title and an image from a file or asset catalog. 41 | /// 42 | /// - Parameters: 43 | /// - title: The title used on this item's generated `UITabBarItem`. 44 | /// - image: The image used on this item's generated `UITabBarItem`. 45 | /// - Returns: The initialized preview object. 46 | public init(title: String, image: UIImage?) { 47 | let viewController = UIViewController() 48 | viewController.tabBarItem = UITabBarItem(title: title, image: image, selectedImage: image) 49 | let label = UILabel() 50 | label.translatesAutoresizingMaskIntoConstraints = false 51 | label.text = "Intentionally blank" 52 | viewController.view.addSubview(label) 53 | label.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor).isActive = true 54 | label.centerYAnchor.constraint(equalTo: viewController.view.centerYAnchor).isActive = true 55 | self.viewController = viewController 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/PreviewView/TabBarControllerPreview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarControllerPreview.swift 3 | // PreviewView 4 | // 5 | // Created by Josh Asbury on 7/8/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A custom parameter attribute that constructs a tab bar from a closure. 11 | /// 12 | /// This is used by ``TabBarControllerPreview`` to provide an ordered list of tab bar items. 13 | @available(iOS 13, *) 14 | @resultBuilder 15 | public enum PreviewTabBarBuilder { 16 | public static func buildBlock(_ items: PreviewTabBarItem...) -> [UIViewController] { items.map(\.viewController) } 17 | } 18 | 19 | /// A type that can be used to preview in Xcode a `UIViewController` embedded within a `UITabBarController`. 20 | /// 21 | /// - SeeAlso: ``ViewControllerPreview`` 22 | /// - SeeAlso: ``NavigationControllerPreview`` 23 | @available(iOS 13, *) 24 | public struct TabBarControllerPreview: UIViewControllerRepresentable { 25 | public let tabBarController: UITabBarController 26 | 27 | /// Creates a tab bar controller preview that displays a view controller at the given position. 28 | /// 29 | /// - Parameters: 30 | /// - selectedIndex: The index within the content closure which locates the element requiring previewing. The default value is `0`. 31 | /// - content: The view controllers of the tab bar controller. 32 | /// - Returns: The initialized preview object. 33 | public init(selectedIndex: Int = 0, @PreviewTabBarBuilder _ content: () -> [UIViewController]) { 34 | let tabBarController = UITabBarController() 35 | tabBarController.viewControllers = content() 36 | tabBarController.selectedIndex = selectedIndex 37 | self.tabBarController = tabBarController 38 | } 39 | 40 | public func makeUIViewController(context: Context) -> some UIViewController { tabBarController } 41 | public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} 42 | } 43 | -------------------------------------------------------------------------------- /Sources/PreviewView/ViewControllerPreview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerPreview.swift 3 | // PreviewView 4 | // 5 | // Created by Josh Asbury on 7/8/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A type that can be used to preview in Xcode a `UIViewController`. 11 | /// 12 | /// ```swift 13 | /// struct YourViewController_Previews: PreviewProvider { 14 | /// static var previews: some View { 15 | /// ViewControllerPreview(YourViewController()) 16 | /// } 17 | /// } 18 | /// ``` 19 | /// 20 | /// - SeeAlso: ``NavigationControllerPreview`` 21 | /// - SeeAlso: ``TabBarControllerPreview`` 22 | @available(iOS 13, *) 23 | public struct ViewControllerPreview: UIViewControllerRepresentable { 24 | /// The view controller being previewed. 25 | public let viewController: UIViewController 26 | 27 | /// Creates a view controller preview. 28 | /// 29 | /// - Returns: The initialized preview object. 30 | public init(_ viewController: UIViewController) { 31 | self.viewController = viewController 32 | } 33 | 34 | public func makeUIViewController(context: Context) -> some UIViewController { viewController } 35 | public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} 36 | } 37 | -------------------------------------------------------------------------------- /Sources/PreviewView/ViewPreview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewPreview.swift 3 | // PreviewView 4 | // 5 | // Created by Josh Asbury on 7/8/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A type that can be used to preview in Xcode a `UIView`. 11 | /// 12 | /// ```swift 13 | /// struct YourView_Previews: PreviewProvider { 14 | /// static var previews: some View { 15 | /// ViewPreview(YourView()) 16 | /// .previewLayout(.fixed(width: 375, height: 86)) 17 | /// } 18 | /// } 19 | /// ``` 20 | /// 21 | /// - Important: For the best possible previewing of a standalone view, you should set the `.previewLayout` values to be the expected in-app sizes of your view. 22 | /// - SeeAlso: ``ViewControllerPreview`` 23 | @available(iOS 13, *) 24 | public struct ViewPreview: UIViewRepresentable { 25 | /// The view being previewed. 26 | public let view: UIView 27 | 28 | /// Creates a view preview. 29 | /// 30 | /// - Returns: The initialized preview object. 31 | public init(_ view: UIView) { 32 | self.view = view 33 | } 34 | 35 | public func makeUIView(context: Context) -> some UIView { view } 36 | public func updateUIView(_ uiView: UIViewType, context: Context) {} 37 | } 38 | -------------------------------------------------------------------------------- /Sources/PreviewViewLegacyOSCompileFix/DesignTime+BackDeploy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DesignTime+BackDeploy.swift 3 | // PreviewViewLegacyOSCompileFix 4 | // 5 | // Created by Josh Asbury on 22/5/2023. 6 | // 7 | 8 | @backDeployed(before: iOS 13) 9 | public func __designTimeString(_ value: String, fallback: String) -> String { 10 | fallback 11 | } 12 | 13 | @backDeployed(before: iOS 13) 14 | public func __designTimeFloat(_: String, fallback: Float) -> Float { 15 | fallback 16 | } 17 | 18 | @backDeployed(before: iOS 13) 19 | public func __designTimeBoolean(_: String, fallback: Bool) -> Bool { 20 | fallback 21 | } 22 | 23 | @backDeployed(before: iOS 13) 24 | public func __designTimeInteger(_: String, fallback: Int) -> Int { 25 | fallback 26 | } 27 | --------------------------------------------------------------------------------