I'm having trouble getting a preview to work with what seems like a pretty simple struct. Customer is a CoreData entity:
struct CustomerDetailView: View {
#Environment(\.managedObjectContext) var moc
#State var showNewCustomer = false
var customer: Customer
var body: some View {
I've tried almost everything that doesn't work, including this:
struct CustomerDetail_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return
CustomerDetailView(customer: --Not sure what works here-- ).environment(\.managedObjectContext, context)
}
}
I've tried static let customer = Customer() so that I would have a customer variable to use in the last line, but that did not help.
Preview uses different container for apps, so you can just create new Customer, like
struct CustomerDetail_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return
CustomerDetailView(customer: Customer(context: context))
.environment(\.managedObjectContext, context)
}
}
Related
I have an array of views stored in a variable that I want to render. This seems pretty straightforward and simple to me -- however, SwiftUI fails to render it. Does anyone know why this is so?
struct TestView: View {
#State private var currentSelectedView = 0
#State private var quote = "Hello 123"
var body: some View { // Failed to produce diagnostic for expression
let viewArray = [FirstTextView(quote:$quote), SecondTextView(quote:$quote)]
viewArray[0]
viewArray[1]
}
}
struct FirstTextView: View {
#Binding var quote:String
var body: some View {
Text(quote)
}
}
struct SecondTextView: View {
#Binding var quote:String
var body: some View {
Text(quote)
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
Try this. The problem was the elements of array were different types.
var body: some View {
let viewArray = [AnyView(FirstTextView(quote:$quote)), AnyView(SecondTextView(quote:$quote))]
viewArray[0]
viewArray[1]
}
When using the SceneDelegate in SwiftUI, it was possible to create a function like the one below that could be used to set the view as shown here. However, in the latest version we now use a WindowsGroup. Is it possible to write a function that changes the view in the WindowsGroup?
func toContentView() {
let contentView = ContentView()
window?.rootViewController = UIHostingController(rootView: contentView)
}
Here is possible alternate approach that do actually the same as your old toContentView
helper class
class Resetter: ObservableObject {
static let shared = Resetter()
#Published private(set) var contentID = UUID()
func toContentView() {
contentID = UUID()
}
}
content of #main
#StateObject var resetter = Resetter.shared
var body: some Scene {
WindowGroup {
ContentView()
.id(resetter.contentID)
}
}
now from anywhere in code to reset to ContentView you can just call
Resetter.shared.toContentView()
I think I'm going about this SwiftUI thing all wrong. It's clear that we're just defining the layout as a structs and there can be limited conventional programming embroiled in the layout. I'm having difficulties thinking like this. What is the best way of doing this?
Take the example below. Project is an NSManagedObject. All I want to do is pass in example record so the SwiftUI will render. Nothing I try works.
struct ProjectView: View
{
#State var project: Project //NSManagedObject
var body: some View
{
TextField("", text: Binding<String>($project.projectName)!)
}
}
struct ProjectView_Previews: PreviewProvider
{
static var previews: some View
{
var p:Project
p = getFirstProject() //returns a Project
return ProjectView(project: p)
}
}
If I try returning the struct it says it cannot preview in the file.
If I don't return the struct I get a Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type error.
UPDATE:
var app = UIApplication.shared.delegate as! AppDelegate
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var persistentContainer: NSPersistentCloudKitContainer = {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
lazy var managedObjectContext: NSManagedObjectContext =
{
return persistentContainer.viewContext
}()
}
And the rest of the code:
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func getCount() -> String
{
let r = allRecords(Project.self)
return String(r.count)
}
struct ProjectView: View
{
// #ObservedObject var project: Project
var body: some View
{
Text(getCount())
// TextField("", text: Binding<String>($project.projectName)!)
}
}
struct ProjectView_Previews: PreviewProvider
{
static var previews: some View
{
ProjectView()
}
}
r.count is returning 0, but in the main application thread it is returning 8. Has app.managedObjectContext not been defined properly? I think this has just got too complicated too quickly.
Assuming getFirstProject works correctly the following should work
struct ProjectView_Previews: PreviewProvider
{
static var previews: some View
{
ProjectView(project: getFirstProject())
}
}
However there are concerns about the following...
struct ProjectView: View
{
#State var project: Project //NSManagedObject
because #State is designed to be internal view state-only thing, but Project in your case is a model, so the recommended scenario for this is to use ObservableObject view model either by conforming Project or as standalone clue class holding Project instance(s).
Proper UIKit Approach:
According to Apple's WWDC 2019 talk on the subject, AVPlayerViewController should be presented modally to take advantage of all the latest full-screen features of the API. This is the recommended sample code to be called from your presenting UIKit view controller:
// Create the player
let player = AVPlayer(url: videoURL)
// Create the player view controller and associate the player
let playerViewController = AVPlayerViewController()
playerViewController.player = player
// Present the player view controller modally
present(playerViewController, animated: true)
This works as expected and launches the video in beautiful full-screen.
Achieve the Same Effect with SwiftUI?:
In order to use the AVPlayerViewController from SwiftUI, I created the UIViewControllerRepresentable implementation:
struct AVPlayerView: UIViewControllerRepresentable {
#Binding var videoURL: URL
private var player: AVPlayer {
return AVPlayer(url: videoURL)
}
func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
playerController.player = player
playerController.player?.play()
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
return AVPlayerViewController()
}
}
I cannot seem to figure out how to present this directly from SwiftUI
in the same way as the AVPlayerViewController is presented directly
from UIKit. My goal is simply to get all of the default, full-screen benefits.
So far, the following has not worked:
If I use a .sheet modifier and present it from within the sheet, then the player is embedded in a sheet and not presented full-screen.
I have also tried to create a custom, empty view controller in UIKit that simply presents my AVPlayerViewController modally from the viewDidAppear method. This gets the player to take on the full screen, but it also shows an empty view controller prior to display the video, which I do not want the user to see.
Any thoughts would be much appreciated!
Just a thought if you like to fullscreen similar like UIKit, did you try the following code from ContentView.
import SwiftUI
import UIKit
import AVKit
struct ContentView: View {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
#State private var vURL = URL(string: "https://www.radiantmediaplayer.com/media/bbb-360p.mp4")
var body: some View {
AVPlayerView(videoURL: self.$vURL).transition(.move(edge: .bottom)).edgesIgnoringSafeArea(.all)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AVPlayerView: UIViewControllerRepresentable {
#Binding var videoURL: URL?
private var player: AVPlayer {
return AVPlayer(url: videoURL!)
}
func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
playerController.modalPresentationStyle = .fullScreen
playerController.player = player
playerController.player?.play()
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
return AVPlayerViewController()
}
}
The solution explained by Razib-Mollick was a good start for me, but it was missing the use of the SwiftUI .sheet() method. I have added this by adding the following to ContentView:
#State private var showVideoPlayer = false
var body: some View {
Button(action: { self.showVideoPlayer = true }) {
Text("Start video")
}
.sheet(isPresented: $showVideoPlayer) {
AVPlayerView(videoURL: self.$vURL)
.edgesIgnoringSafeArea(.all)
}
}
But the problem is then, that the AVPlayer is instantiated again and again when SwiftUI re-renders the UI.
Therefore the state of the AVPlayer has to move to a class object stored in the environment, so we can get hold of it from the View struct. So my latest solution looks now as follows. I hope it helps somebody else.
class PlayerState: ObservableObject {
public var currentPlayer: AVPlayer?
private var videoUrl : URL?
public func player(for url: URL) -> AVPlayer {
if let player = currentPlayer, url == videoUrl {
return player
}
currentPlayer = AVPlayer(url: url)
videoUrl = url
return currentPlayer!
}
}
struct ContentView: View {
#EnvironmentObject var playerState : PlayerState
#State private var vURL = URL(string: "https://www.radiantmediaplayer.com/media/bbb-360p.mp4")
#State private var showVideoPlayer = false
var body: some View {
Button(action: { self.showVideoPlayer = true }) {
Text("Start video")
}
.sheet(isPresented: $showVideoPlayer, onDismiss: { self.playerState.currentPlayer?.pause() }) {
AVPlayerView(videoURL: self.$vURL)
.edgesIgnoringSafeArea(.all)
.environmentObject(self.playerState)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(PlayerState())
}
}
struct AVPlayerView: UIViewControllerRepresentable {
#EnvironmentObject var playerState : PlayerState
#Binding var videoURL: URL?
func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
let playerController = AVPlayerViewController()
playerController.modalPresentationStyle = .fullScreen
playerController.player = playerState.player(for: videoURL!)
playerController.player?.play()
return playerController
}
}
Something to be aware of (a bug?): whenever a modal sheet is shown using .sheet() the environment objects are not automatically passed to the subviews. They have to be added using environmentObject().
Here is a link to read more about this problem: https://oleb.net/2020/sheet-environment/
Xcode 12 · iOS 14
→ Use .fullScreenCover instead of .sheet and you’re good to go.
See also: How to present a full screen modal view using fullScreenCover
28-07-2019. I still have a question about the code below. I would like to separate the data model out of the ContentView. So I made a separate file and added the class, like this:
import SwiftUI
import Combine
class User: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var username : String = "Jan" { willSet { willChange.send() }}
var password : String = "123456" { willSet { willChange.send() } }
var emailAddress : String = "jan#mail.nl" { willSet { willChange.send() } }
}
#if DEBUG
struct User_Previews: PreviewProvider {
static var previews: some View {
User()
.environmentObject(User())
}
}
#endif
This doesn't work however, I'm getting an error:
Protocol type 'Any' cannot conform to 'View' because only concrete types can conform to protocols
Error occurs on the .environmentObject(User()) line in # if DEBUG.
After watching some video's I made the following code (including changes for Xcode 11 beta 4). Tips from both answers from dfd and MScottWaller are already included in the code.
import Combine
import SwiftUI
class User: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var username = "Jan" { willSet { willChange.send() } }
var password = "123456" { willSet { willChange.send() } }
var emailAddress = "jan#mail.nl" { willSet { willChange.send() } }
}
struct ContentView: View {
#EnvironmentObject var user: User
private func buttonPressed() {
print(user.username) // in Simulator
}
var body: some View {
VStack {
TextField("Username", text: $user.username)
TextField("Password", text: $user.password)
TextField("Emailaddress", text: $user.emailAddress)
Button(action: buttonPressed) {
Text("Press me!")
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(User())
}
}
#endif
But now onto the next part. If I have another view... how can I reference the data then? Since the source of truth is in the above ViewContent() view. The answer is:
import SwiftUI
struct DetailView: View {
#EnvironmentObject var user: User
var body: some View {
VStack {
TextField("Username", text: $user.username)
TextField("Password", text: $user.password)
TextField("Email", text: $user.emailAddress)
}
}
}
#if DEBUG
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView()
.environmentObject(User())
}
}
#endif
Don't forget to edit the SceneDelegate (answer from dfd):
var user = User()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView()
.environmentObject(user)
)
self.window = window
window.makeKeyAndVisible()
}
}
In your DetailView preview, don't for get to attach the environmentObject. See how I've added it in the PreviewProvider below. When you run the actual app, you'll want to do the same to you ContentView in the SceneDelegate
import SwiftUI
struct DetailView: View {
#EnvironmentObject var user: User
var body: some View {
HStack {
TextField("Username", text: $user.username)
Text("Hello world!")
}
}
}
#if DEBUG
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView()
.environmentObject(User())
}
}
#endif
If the "source of truth" is User, and you've made it a BindableObject you just need to expose it best to make it available to the various views you want. I suggest #EnvironmentObject.
In your SceneDelegate, do this:
var user = User()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView()
.environmentObject(user)
)
self.window = window
window.makeKeyAndVisible()
}
}
Now that a "stateful" instance of User is available to any View, you simply need to add:
#EnvironmentObject var user: User
To any/all vies that need to know about User.
BindableObject (for the most part) reserve memory for what you've denied. #ObjectBinding merely binds a view to what is in that part of memory (again, for the most part). And yes, you can do this for User in all views - but since you are instantiating it in ContentView? Nope.)! #EnvironmentObject makes it available to any views that need to access it.
Absolutely, you can use #ObjectBinding instead of an #EnvironmentObject, but so far,? I've never heard of a reason to do that.