I have a simple CardView container that gets a view in its initializer.
struct CardView<Content: View>: View {
#ViewBuilder var content: Content
var body: some View {
content
.background(Color.green)
.cornerRadius(20)
}
}
When I use it in a view body it works ✅:
struct ScreenView: View {
var body: some View {
CardView {
Text("Hello!")
.padding()
}
}
}
but when I try to keep a reference to the view I get an error:
let card = CardView {
Text("Hello!")
.padding()
}
Related
I'm running into an issue with the navigation title header in SwiftUI. It's a combination of a couple of things, as far as I can tell...
The main problem is that I'm trying to change the default background color of a view that contains a list. But when I use the tag .background(), the navigation title background becomes transparent. This only happens when there is a VStack on the view.
I have a simplify example code that shows the problem I'm facing:
ContentView:
import SwiftUI
struct ContentView: View {
#State var showButton: Bool
var body: some View {
VStack {
NavigationStack {
NavigationLink(
destination: SecondView(showButton: showButton),
label: {
Text("Take me to second view")
})
Toggle("VStack Visibile", isOn: $showButton)
.padding()
}
}
}
}
SecondView:
import SwiftUI
struct SecondView: View {
#State private var isButtonVisible: Bool = false
#State var showButton: Bool = true
var body: some View {
VStack {
List(0..<10) { _ in
Text("Hello World")
}
if showButton {
button
}
}
.navigationTitle("This is a title")
.background(Color(.systemCyan))
}
var button: some View {
Text("Something")
}
}
Please see below the resulting problem:
Issues / Suggestions:
ContentView
Have the NavigationStack outside the VStack
SecondView
Don't embed List inside a VStack
List is special and has special characteristics
Don't initialise #State property from outside, pass a binding instead
Code:
ContentView:
struct ContentView: View {
#State var showButton = true
var body: some View {
NavigationStack {
VStack {
NavigationLink(
destination: SecondView(showButton: $showButton),
label: {
Text("Take me to second view")
})
Toggle("VStack Visibile", isOn: $showButton)
.padding()
}
}
}
}
SecondView
struct SecondView: View {
#State private var isButtonVisible: Bool = false
#Binding var showButton: Bool
var body: some View {
List {
ForEach(0..<100) { _ in
Text("Hello World")
}
}
.safeAreaInset(edge: .bottom) {
if showButton {
HStack {
Spacer()
button
Spacer()
}
//I have added transparency, you can make it opaque if you want
.background(.cyan.opacity(0.8))
}
}
}
var button: some View {
Text("Something")
}
}
Try this if you don't want your list go under nav bar.
struct SecondView: View {
#State private var isButtonVisible: Bool = false
#State var showButton: Bool = true
var body: some View {
VStack {
List(0..<10) { _ in
Text("Hello World")
}
.padding(.top, 1)
if showButton {
button
}
}
.background(Color(.systemCyan))
.navigationTitle("This is a title")
}
var button: some View {
Text("Something")
}
}
Video Link Youtube
ConsoleError
Fatal error: No ObservableObject of type UserDefaultsConfig found. A View.environmentObject(_:) for UserDefaultsConfig may be missing as an ancestor of this view.: file SwiftUI, line 0
UserDefaults Config
class UserDefaultsConfig: ObservableObject {
#Published var allFavoriteMovie: [PopularMovie] = []
let defaults = UserDefaults.standard
func setUserDefaults(value: PopularMovie) {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(value) {
defaults.set(encoded, forKey: "favoriteMovie")
}
if let savedFavoriteMovie = defaults.object(forKey: "favoriteMovie") as? Data {
let decoder = JSONDecoder()
if let loadedFavMovie = try? decoder.decode(PopularMovie.self, from: savedFavoriteMovie) {
allFavoriteMovie.append(loadedFavMovie)
print("favMovie: \(allFavoriteMovie)")
}
}
}
}
CustomTabView opens first
CustomTabView
TabView(selection: $selectedTab) {
MainView(searchMovie: SearchMovieVM(), mainVM: MainVM())
.ignoresSafeArea()
.tag(TabCategory.movieList)
FavoriteView()
.ignoresSafeArea()
.tag(TabCategory.favorite)
}
I click on MovieCell in MainView and direct it to DetailView.
MainView
NavigationLink(
destination: DetailView(movie: .constant(mainVM.popularMovie[movie])),
label: {
MovieCell(popularMovie: .constant(mainVM.popularMovie[movie]))
.foregroundColor(Color(UIColor.label))
})
DetailView
When I clicked the button here, setUserDefaults function in userDefaultsConfig was running.
struct DetailView: View {
....
#StateObject var userDefaultsConfig = UserDefaultsConfig()
var body: some View {
ZStack {
.....
ScrollView(.vertical, showsIndicators: false) {
.....
HStack {
Spacer()
Button(action: { self.userDefaultsConfig.setUserDefaults(value: movie) }) {
Image(systemName: "suit.heart")
.imageScale(.large)
.foregroundColor(.red)
}
}
....
}
.padding(.horizontal)
}
}
.environmentObject(userDefaultsConfig)
}
}
The movie saved in UserDefaults on the DetailView page will be shown on the Favorites page, but when I open the Favorites page, I get an error.
FavoritesView
struct FavoriteView: View {
#EnvironmentObject var userDefaultsConfig: UserDefaultsConfig
var body: some View {
VStack {
// ForEach(userDefaultsConfig.allFavoriteMovie, id: \.id) { item in
// Text(item.title ?? "")
// }
Text("")
.onAppear {
print(self.userDefaultsConfig.allFavoriteMovie)
}
}
}
}
You need to place state object in CustomTabView, like
struct CustomTabView: View {
....
#StateObject var userDefaultsConfig = UserDefaultsConfig()
...
TabView(selection: $selectedTab) {
MainView(searchMovie: SearchMovieVM(), mainVM: MainVM())
.ignoresSafeArea()
.tag(TabCategory.movieList)
FavoriteView()
.ignoresSafeArea()
.tag(TabCategory.favorite)
}
.environmentObject(userDefaultsConfig) // << here !!
...
and in DetailView just use it as environment object, same as you do in FavoriteView, ie.
struct DetailView: View {
#EnvironmentObject var userDefaultsConfig: UserDefaultsConfig
...
}
I have created a Swift Package that creates multiple PageTabViews from an array of content that is passed to it:
import SwiftUI
public struct WhatsNewView<Content: View>: View {
let content: [Content]
public init(content: [Content]){
self.content = content
}
public var body: some View {
TabView {
ForEach(0..<content.count, id: \.self) { pageNum in
WhatsNewPage(content: content[pageNum], pageNum: pageNum + 1, totalPages: content.count)
}
}
.background(Color.white)
.ignoresSafeArea()
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
When I call it I want to pass in any kind of simple or complex view to fill each page. Right now, I can pass in an array of simple text views like so:
import SwiftUI
import WhatsNew
#main
struct mFood_Vendor: App {
#State var showWhatsNew = false
let whatsNew = WhatsNew()
var page1 = Text("Hello World")
var page2 = Text("Goodbye World")
var body: some Scene {
WindowGroup {
ContentView()
.fullScreenCover(isPresented: $showWhatsNew, content: {
let content = [page1, page2]
WhatsNewView(content: content)
})
.onAppear(perform: {
whatsNew.checkForUpdate(showWhatsNew: $showWhatsNew)
})
}
}
}
I want page1 and page2 to be whatever content a person wants to see on the What's New pages. But if I change those vars to anything different, like a text and an Image, I get a "Failed to produce diagnostic for expression" error.
Ideally, I would like to be able to pass in something like:
struct page1: View {
var body: some View {
VStack {
Text("something")
Image("plus")
}
}
}
Any help would be appreciated. THANKS!
You can use AnyView to get what you want. In that case your code would become:
public struct WhatsNewView: View {
let content: [AnyView]
public init(content: [AnyView]){
self.content = content
}
public var body: some View {
TabView {
ForEach(0..<content.count, id: \.self) { pageNum in
WhatsNewPage(content: content[pageNum], pageNum: pageNum + 1, totalPages: content.count)
}
}
.background(Color.white)
.ignoresSafeArea()
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
And, as an example of usage:
struct ContentView: View {
var body: some View {
let view1 = AnyView(Text("Hello World"))
let view2 = AnyView(Image(systemName: "star.fill"))
return WhatsNewView(content: [view1, view2])
}
}
EDIT: I've just found out that TabView can be built out of a TupleView. This means that, depending on your needs, you can write something like this (which would be great because it doesn't force your Swift Package users to wrap all the views inside AnyView):
import SwiftUI
public struct WhatsNewView<Content: View>: View {
private let content: Content
public init(#ViewBuilder contentProvider: () -> Content){
content = contentProvider()
}
public var body: some View {
TabView {
content
}
.background(Color.white)
.ignoresSafeArea()
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
You can use it like this:
struct ContentView: View {
var body: some View {
WhatsNewView {
Text("Hello World")
Image(systemName: "star.fill")
Color.red
VStack {
Text("Text1")
Text("Text2")
Text("Text3")
}
Button("Tap Me") {
print("Tapped")
}
Group {
Text("Group1")
Text("Group2")
}
}
}
}
The result is:
Turns out that if I leave WhatsNewView alone, I can just do the following:
struct mFood_Vendor: App {
#State var showWhatsNew = false
let whatsNew = WhatsNew()
var page1: some View {
VStack (alignment: .leading){
Text("Here is a list of new features:")
Text("Hello World")
Image(systemName: "plus")
}
}
var page2: some View {
Text("Goodbye World")
}
var body: some Scene {
WindowGroup {
ContentView()
.fullScreenCover(isPresented: $showWhatsNew, content: {
let content = [AnyView(page1), AnyView(page2)]
WhatsNewView (content: content)
})
.onAppear(perform: {
whatsNew.checkForUpdate(showWhatsNew: $showWhatsNew)
})
}
}
}
Basically just wrap each content page in AnyView.
I've created a custom view which is basically a VStack with modifiers applied to it. But unlike the original VStack view, I have to use a grouping view when I'm using it with multiple subviews.
How can I get rid of the "Group" in the below example?
import SwiftUI
struct ContentView: View {
var body: some View {
CustomGroup() {
Group {
Text("Hello")
Text("World")
}
}
}
}
struct CustomGroup<Content>: View where Content : View {
let content: () -> Content
var body: some View {
VStack() {
content()
}
.background(Color.yellow)
.cornerRadius(8)
}
}
You need init with ViewBuilder
Here is a solution. Tested with Xcode 12 / iOS 14
struct TestCustomGroup: View {
var body: some View {
CustomGroup {
Text("Hello")
Text("World")
}
}
}
struct CustomGroup<Content>: View where Content : View {
let content: () -> Content
init(#ViewBuilder _ content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack {
content()
}
.background(Color.yellow)
.cornerRadius(8)
}
}
I have following View and I need to pass item content to another View (DetailsEvent.swift), I am using NavigationLink . (I'm using Xcode 11 GM)
struct Events: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView{
List(self.networkManager.eventos.events){ item in
NavigationLink(destination: DetailsEvent(item: item)) {
VStack(alignment: .leading) {
HStack {
Image(systemName: "calendar")
.foregroundColor(.gray)
Text(item.start_date)
.font(.subheadline)
.foregroundColor(.gray)
}
Text(item.title)
.font(.headline)
}
}
}
.navigationBarTitle("Events")
}
}
But it gives me the following error:
Argument passed to call that takes no arguments
Without passing any variable: NavigationLink (destination: DetailsEvent () everything works fine.
DetailsEvent.swift
struct DetailsEvent: View {
var body: some View {
Text("here details content")
}
}
Your struct has no property called item. You need something like this:
struct DetailsEvent: View {
let item: Event
var body: some View {
Text("here details about \(item.title)")
}
}