I'm playing with the new Xcode 12 beta & SwiftUi 2.0. .matchedGeometryEffect() modifier is great to do Hero animations. There is a new property #Namespace is introduced in SwiftUI. Its super cool. working awesome.
I was just wondering if there is any possibility to pass a Namespace variable to multiple Views?
Here is an example I'm working on,
struct HomeView: View {
#Namespace var namespace
#State var isDisplay = true
var body: some View {
ZStack {
if isDisplay {
VStack {
Image("share sheet")
.resizable()
.frame(width: 150, height: 100)
.matchedGeometryEffect(id: "img", in: namespace)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
.onTapGesture {
withAnimation {
self.isDisplay.toggle()
}
}
} else {
VStack {
Spacer()
Image("share sheet")
.resizable()
.frame(width: 300, height: 200)
.matchedGeometryEffect(id: "img", in: namespace)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.onTapGesture {
withAnimation {
self.isDisplay.toggle()
}
}
}
}
}
}
It is working fine.
But if I want to extract the Vstack as a SubView, Below picture shows that I have extracted the first VStack into a subview.
I'm getting a compliment Cannot find 'namespace' in scope
Is there a way to pass namespace across multiple Views?
The #Namespace is a wrapper for Namespace.ID, and you can pass Namespace.ID in argument to subviews.
Here is a demo of possible solution. Tested with Xcode 12 / iOS 14
struct HomeView: View {
#Namespace var namespace
#State var isDisplay = true
var body: some View {
ZStack {
if isDisplay {
View1(namespace: namespace, isDisplay: $isDisplay)
} else {
View2(namespace: namespace, isDisplay: $isDisplay)
}
}
}
}
struct View1: View {
let namespace: Namespace.ID
#Binding var isDisplay: Bool
var body: some View {
VStack {
Image("plant")
.resizable()
.frame(width: 150, height: 100)
.matchedGeometryEffect(id: "img", in: namespace)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
.onTapGesture {
withAnimation {
self.isDisplay.toggle()
}
}
}
}
struct View2: View {
let namespace: Namespace.ID
#Binding var isDisplay: Bool
var body: some View {
VStack {
Spacer()
Image("plant")
.resizable()
.frame(width: 300, height: 200)
.matchedGeometryEffect(id: "img", in: namespace)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.onTapGesture {
withAnimation {
self.isDisplay.toggle()
}
}
}
}
A warning free approach to inject the Namespace into the Environment is to create an ObservableObject, named something like NamespaceWrapper, to hold the Namespace once it's been created. This could look something like:
class NamespaceWrapper: ObservableObject {
var namespace: Namespace.ID
init(_ namespace: Namespace.ID) {
self.namespace = namespace
}
}
You would then create and pass the Namespace like so:
struct ContentView: View {
#Namespace var someNamespace
var body: some View {
Foo()
.environmentObject(NamespaceWrapper(someNamespace))
}
}
struct Foo: View {
#EnvironmentObject var namespaceWrapper: NamespaceWrapper
var body: some View {
Text("Hey you guys!")
.matchedGeometryEffect(id: "textView", in: namespaceWrapper.namespace)
}
}
While the accepted answer works, it gets a bit annoying to share the namespace across multiple nested subviews, especially if you'd like your initialisers clean and to the point. Using environment values might be better in this case:
struct NamespaceEnvironmentKey: EnvironmentKey {
static var defaultValue: Namespace.ID = Namespace().wrappedValue
}
extension EnvironmentValues {
var namespace: Namespace.ID {
get { self[NamespaceEnvironmentKey.self] }
set { self[NamespaceEnvironmentKey.self] = newValue }
}
}
extension View {
func namespace(_ value: Namespace.ID) -> some View {
environment(\.namespace, value)
}
}
Now you can create a namespace in any view and allow all its descendants to use it:
/// Main View
struct PlaygroundView: View {
#Namespace private var namespace
var body: some View {
ZStack {
SplashView()
...
}
.namespace(namespace)
}
}
/// Subview
struct SplashView: View {
#Environment(\.namespace) var namespace
var body: some View {
ZStack(alignment: .center) {
Image("logo", bundle: .module)
.matchedGeometryEffect(id: "logo", in: namespace)
}
}
}
A slight evolution on #mickben 's answer.
We'll use a Namespaces object to hold multiple Namespace.ID instances.
We'll inject that as an environment object - and also provide an easy way to configure Previews.
Firstly - the Namespaces wrapper
class Namespaces:ObservableObject {
internal init(image: Namespace.ID = Namespace().wrappedValue,
title: Namespace.ID = Namespace().wrappedValue) {
self.image = image
self.title = title
}
let image:Namespace.ID
let title:Namespace.ID
}
I use different namespaces for different objects.
So, an Item with an image & title...
struct Item:Identifiable {
let id = UUID()
var image:UIImage = UIImage(named:"dummy")!
var title = "Dummy Title"
}
struct ItemView: View {
#EnvironmentObject var namespaces:Namespaces
var item:Item
var body: some View {
VStack {
Image(uiImage: item.image)
.matchedGeometryEffect(id: item.id, in: namespaces.image)
Text(item.title)
.matchedGeometryEffect(id: item.id, in: namespaces.title)
}
}
}
struct ItemView_Previews: PreviewProvider {
static var previews: some View {
ItemView(item:Item())
.environmentObject(Namespaces())
}
}
You can see the advantage of multiple namespaces here. I can use the asme item.id as the 'natural' id for both the image and title animations.
Notice how the preview is really easy to construct here using .environmentObject(Namespaces())
If we use Namespaces() to create our namespaces in the actual app, then we'll get a warning.
Reading a Namespace property outside View.body. This will result in
identifiers that never match any other identifier.
Depending on your setup - this may not be true, but we can work around by using the explicit initialiser
struct ContentView: View {
var body: some View {
ItemView(item: Item())
.environmentObject(namespaces)
}
#Namespace var image
#Namespace var title
var namespaces:Namespaces {
Namespaces(image: image, title: title)
}
}
I like to keep my namespace creation in a var as it keeps the property wrappers and initialiser together. It's easy to add new namespaces as appropriate.
Related
I'm learning swiftUI and I want to make a music app.
I created a view which going to be above the tabView, but I want it to be shown only if user start playing a music.
My App, I use ZStack for bottomPlayer, and I share the bottomPlayer variable through .environmentObject(bottomPlayer) so the child views can use it:
class BottomPlayer: ObservableObject {
var show: Bool = false
}
#main
struct MyCurrentApp: App {
var bottomPlayer: BottomPlayer = BottomPlayer()
var audioPlayer = AudioPlayer()
var body: some Scene {
WindowGroup {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
TabBar()
if bottomPlayer.show {
BottomPlayerView()
.offset(y: -40)
}
}
.environmentObject(bottomPlayer)
}
}
}
The BottomPlayerView (above the TabView)
struct BottomPlayerView: View {
var body: some View {
HStack {
Image("cover")
.resizable()
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
Text("Artist")
.foregroundColor(.orange)
Text("Song title")
.fontWeight(.bold)
}
Spacer()
Button {
print("button")
} label: {
Image(systemName: "play")
}
.frame(width: 60, height: 60)
}
.frame(maxWidth: .infinity, maxHeight: 60)
.background(Color.white)
.onTapGesture {
print("ontap")
}
}
}
My TabView:
struct TabBar: View {
var body: some View {
TabView {
AudiosTabBarView()
VideosTabBarView()
SearchTabBarView()
}
}
}
And In my SongsView, I use the EnvironmentObject to switch on the bottomPlayerView
struct SongsView: View {
#EnvironmentObject var bottomPlayer: BottomPlayer
var body: some View {
NavigationView {
VStack {
Button {
bottomPlayer.show = true
} label: {
Text("Show Player")
}
}
.listStyle(.plain)
.navigationBarTitle("Audios")
}
}
}
The problem is the bottomPlayer.show is actually set to true, but doesn't appear ...
Where I am wrong?
In your BottomPlayer add theĀ #Published attribute before the show boolean.
This creates a publisher of this type.
apple documentation
I have two views, ViewAssignment and TaskDetailView. My ViewAssignment page fetches data from an environment object, and creates a list using the data.
Upon each item of the list being clicked on, the TaskDetailView pops in as a navigation link, however, I am having trouble making the information in the TaskDetailView unique to that particular iteration (the item in the list)
I believe the trouble comes from my TaskDetailView.swift
import SwiftUI
struct TaskDetailView: View {
#EnvironmentObject var assignment: Assignments
#State var taskNotes = ""
var body: some View {
VStack(spacing: 10) {
Image("english-essay")
.resizable()
.scaledToFit()
.frame(width: 250, height: 160)
.cornerRadius(20)
Text(self.assignment.data.first?.taskName ?? "Untitled Task")
.font(.title2)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.center)
HStack(spacing: 20) {
Label(self.assignment.data.first?.weighting ?? "0", systemImage: "percent")
.font(.subheadline)
.foregroundColor(.secondary)
Text(self.assignment.data.first?.dueDate ?? "No Date")
.font(.subheadline)
.foregroundColor(.secondary)
}
TextField("Write any notes here", text: $taskNotes)
.font(.body)
.padding()
Spacer()
}
}
}
struct TaskDetailView_Previews: PreviewProvider {
static var previews: some View {
TaskDetailView() // I assume there is some information I have to pass through here
}
}
For details, this is my other view:
import SwiftUI
struct ViewAssignment: View {
// Observed to update the UI
#EnvironmentObject var assignment: Assignments
var body: some View {
ZStack {
NavigationView {
List(self.assignment.data) { task in
NavigationLink (
destination: TaskDetailView(),
label: {
Image(systemName: "doc.append.fill")
.scaleEffect(2.5)
.padding()
VStack(alignment: .leading, spacing: 3) {
Text(task.taskName)
.fontWeight(.semibold)
.lineLimit(2)
Text(task.dueDate + " - " + task.subject)
.font(.subheadline)
.foregroundColor(.secondary)
}
})
}
.navigationTitle("My Tasks")
.listStyle(InsetGroupedListStyle())
}
}
}
}
struct ViewAssignment_Previews: PreviewProvider {
static var previews: some View {
ViewAssignment()
}
}
I would also like to know if, upon making the screen unique for each item in the list, would I be able to have the contents of the text field saved upon reloading the app, Perhaps through #AppStorage?
Thank you for the assistance.
If I understand correctly what you are trying to do:
a TaskDetailView displays the detail of a ... Task.
So you should have a Task structure like this:
struct Task {
let name: String
let subject: String
...
}
You have to create one (or more) instance of Task to test your TaskDetailView:
extension Task {
var test: Task {
Task(name: "Test", subject: "Test Subject")
}
}
Now in the preview of your TaskDetailView you can try to display your example :
struct TaskDetailView_Previews: PreviewProvider {
static var previews: some View {
TaskDetailView(task: Task.test) // here
}
}
For the moment nothing is happening. Because your TaskDetailView doesn't have a task parameter.
struct TaskDetailView: View {
var task: Task
var body: some View {
...
}
Now its body can use the different parameters of this Task.
Text(task.name)
.font(.title2)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.center)
Now in your List:
List(self.assignment.data) { task in
NavigationLink (
destination: TaskDetailView(task: task), // <- here !!!
label: {
Image(systemName: "doc.append.fill")
.scaleEffect(2.5)
.padding()
VStack(alignment: .leading, spacing: 3) {
Text(task.name)
.fontWeight(.semibold)
.lineLimit(2)
}
})
}
I'm trying to pass a few simple data (strings and URL and images) from one view to another view in my swiftui app.
This simple task proves to be a headache at the moment.
This is what I have done so far:
in my firstView I have this:
struct Book {
var title: String
var author: String
}
struct firstView: View {
#State private var showAudioPlayer = false
#Binding var tabSelection: Int
init(tabSelection: Binding<Int>) {
_tabSelection = tabSelection
}
var body: some View {
NavigationView {
ZStack {
Text("Tap Here")
.onTapGesture {
self.showAudioPlayer.toggle()
}
}
}
.fullScreenCover(isPresented: $showAudioPlayer, content: secondView.init)
}
}
And this is what I have in my secondView:
struct secondView: View {
#Environment(\.presentationMode) var presentationMode
var book: Book
var body: some View {
NavigationView {
ZStack{
VStack {
}
}
.background(Color.clear)
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(
leading: Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "chevron.down.circle")
.foregroundColor(Color.black)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
}
.edgesIgnoringSafeArea(.all)
}
}
However, when I try to compile my code, I get this error:
The error itself doesn't make much sense and I'm stuck.
what am I doing wrong?
Memberwise Initializers for Structure Types
The compiler takes a peak into the struct and sees two non-optional properties and thinks they should be in the constructor. It doesn't know the #Environment is going to magically be set by the framework, so it includes that property. As #Baglan said above, you are on the hook for the Book and need to pass it in.
The error message is saying "you asked me to instance audioPlayerView and I need a couple of parameters to make that happen", but you really only need one.
Anyone know how to make a dynamic pageView controller in SwiftUI, iOS 14? Something that displays pages that are a function of their date so that one can scroll left or right to look at data from the past, present and future.
struct DatePg: View
{
let date: Date
var body: some View {
Text(date.description)
}
}
There is a new API that allows one to make a PageViewController with the TabView and a viewModifier. But the only examples I've seen are static. Here's an example of a static PageView.
import SwiftUI
struct SwiftUIPageView: View
{
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
Text("Hello")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
.tag(0)
Text("World")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.tag(1)
}.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
Already have something working using UIHostingController but passing NSManageObjectContext through the UIKit objects is cuasing problems.
Here's where I'm at so far. Still not working.
import SwiftUI
#main struct PagerApp: App
{
var body: some Scene {
WindowGroup { DatePageView() }
}
}
struct DatePageView: View
{
#StateObject var dateData = DateData(present: Date())
#State var index: Int = 1
var body: some View {
TabView(selection: $index) {
ForEach(dateData.dates, id: \.self) { date in
Text(date.description)
.onAppear { dateData.current(date: date) }
.tag(dateData.tag(date: date))
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
class DateData: ObservableObject
{
#Published var dates: [Date]
init(present: Date) {
let past = present.previousDay()
let future = present.nextDay()
self.dates = [past, present, future]
}
func current(date: Date) {
//center around
guard let i = dates.firstIndex(of: date) else { fatalError() }
self.dates = [ dates[i].previousDay(), dates[i], dates[i].nextDay() ]
print("make item at \(i) present")
}
func tag(date: Date) -> Int {
guard let i = dates.firstIndex(of: date) else { fatalError() }
return i
}
}
You can create view dynamically using an array and ForEach.
Here is an example using an array of strings:
// See edited section
You could pass the items you want in the View initializer
Edit:
Here is an example for adding a new page each time I reach the last one:
struct SwiftUIPageView: View
{
#State private var selection = "0"
#State var items: [String] = ["0", "1", "2", "3"]
var body: some View {
TabView(selection: $selection) {
ForEach(items, id: \.self) { item in
Text(item)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.tag(item)
}
}.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.onChange(of: selection, perform: { value in
if Int(value) == (self.items.count - 1) {
self.items.append("\(self.items.count)")
}
})
.id(items)
}
}
The last id(items) is important because it forces the View to reload when the array changes.
I am trying to pass data (incomeAmount) from the First View to the Second View in my SwiftUI app, but I need to declare the BindingString. What does it mean that I need to declare the BindingString?
View 1
struct ContentView: View {
#State var incomeAmount = ""
var body: some View {
NavigationView {
VStack {
TextField("Your income amount", text: $incomeAmount)
.frame(width: 300)
.padding(.bottom, 30)
NavigationLink(destination: NewView()){
Text("Continue")
.frame(width: 300, height: 50, alignment: .center)
.background(Color.black)
.cornerRadius(130)
.foregroundColor(.white)
.padding(.bottom, 30)
}
Text("$\(incomeAmount)")
}.navigationTitle("First View")
}
}
}
View 2
struct NewView: View {
#Binding var incomeAmount: String
var body: some View {
NavigationView {
VStack {
Text("$\(incomeAmount)")
}
}.navigationTitle("Second View")
}
}
struct NewView_Previews: PreviewProvider {
static var previews: some View {
NewView(incomeAmount: <#Binding<String>#>)
}
}
To pass the incomeAmount to the second view, you have to do this:
NavigationLink(destination: NewView(incomeAmount: $incomeAmount)) { ... }
Also, you need to provide it to:
struct NewView_Previews: PreviewProvider {
static var previews: some View {
NewView(incomeAmount: .constant("something here"))
}
}
Because in PreviewProvider, NewView expect to have a Binding<String> passed in. So you need to provide it.