SwiftUI: Is it possible to layer these views without each one being scrollable? - ios

I am presenting a sheet that will present a slider within a section which is within a form. I was trying to add my subview AddDoujin into this view as well. But the problem is, is that both views are scrollable and aren't layering on top of each other and seems like it is being added to the bottom (if that makes sense). My goal is to make the AddDoujin view right under the slider rather than both being each their own view and both of them being scrollable. Sorry if this is unclear and you need more information.
import SwiftUI
struct TestingAddDoujin: View {
//Varaibles
#State private var InputDoujin:String = ""
var DoujinApi:DoujinAPI
#Binding var isPresented:Bool
#State private var RedoEntry:Bool = false
var PickerOptions = ["Doujin", "Hentai"]
#State var PickerSelected = ""
#State var CurrentSelectionForPicker = 0
var body: some View {
GeometryReader { geo in
NavigationView{
VStack {
ZStack{
Form{
Section(header: Text("What you you looking for?")) {
Picker(selection: $CurrentSelectionForPicker, label: Text("Please select one")) {
ForEach(0..<PickerOptions.count) {
Text("\(self.PickerOptions[$0])")
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
AddDoujin(DoujinApi: DoujinApi, isPresented: $isPresented)
}
}
}
}
}
struct TestingAddDoujin_Previews: PreviewProvider {
static var previews: some View {
TestingAddDoujin(DoujinApi: DoujinAPI(), isPresented: .constant(false))
}
}

It seems like if you just move AddDoujin(DoujinApi: DoujinApi, isPresented: $isPresented) right below .pickerStyle(SegmentedPickerStyle()) You will get the desired result.
I'm guessing you've tried this. So maybe show and tell us more about what you are trying to accomplish.
btw. In Swift it's not convention to capitalize the first letter of a variable.

Related

SwiftUI #State variable does not change view

Using HalfASheet (https://github.com/franklynw/HalfASheet).
I have a View called ProjectsView, and in the ZStack in ProjectsView I have ProjectSorting and SortingView(both injected with the EnvironmentObject). I want the Text(🟩) in ProjectSorting to be changed, and the HStack(🟦) in SortingView to have a checkmark, both depending on the value of the sorting variable in SortingValues. Users can change the value of the sorting by pressing the Button in SortingView.
For whatever reason, the Text(🟩) in ProjectSorting does not change at all. And the HStack(🟦) in SortingView only gets the checkmark when its ancestor stack has another Text(🟨) which includes the #State variable from the environment, which I find very weird.
What should I change? Is there any way I can make this work using #EnvironmentObject? I'm a newbie and couldn't really understand other wrappers so I'd like to make this work within #State, #Binding, #EnvirionmentObject.
Thanks in advance.
SortingValues.swift
import Combine
class SortingValues: ObservableObject {
#Published var sorting = "Top Rated"
}
ProjectsView.swift
struct ProjectsView: View {
#Binding var isPresented: Bool
#State var showSortingSheet = false
var body: some View {
ZStack {
NavigationView {
VStack(spacing: 0) {
ProjectsTopView(isPresented: $isPresented)
ProjectSorting(showSortingSheet: $showSortingSheet)
.environmentObject(SortingValues())
ProjectList()
}
.navigationBarHidden(true)
}
SortingView(showSortingSheet: $showSortingSheet)
.environmentObject(SortingValues())
}
}
}
ProjectSorting.swift
import SwiftUI
struct ProjectSorting: View {
#EnvironmentObject var sortingValues: SortingValues
#Binding var showSortingSheet: Bool
#State var sortingValue = ""
var body: some View {
VStack {
HStack {
Text("Projects")
Spacer()
Button {
showSortingSheet.toggle()
} label: {
HStack(spacing: 3) {
Image("sortingArrows")
Text(sortingValue) // < 🟩 this is the Text I want to be changed
}
}
}
// Another HStack goes here
}
.onReceive(sortingValues.$sorting) { sorting in
print("This is ProjectSorting. sorting:", sorting) // < this does not print when I close the half sheet
sortingValue = sorting
}
}
}
SortingView.swift
import SwiftUI
import HalfASheet
struct SortingView: View {
#EnvironmentObject var sortingValues: SortingValues
#Binding var showSortingSheet: Bool
#State var sortingValue = ""
var body: some View {
VStack {
HalfASheet(isPresented: $showSortingSheet) {
let sorting = ["Most Recent", "Most Reviewed", "Top Rated", "Lowest Price", "Highest Price"]
VStack(alignment: .leading) {
ForEach(sorting, id: \.self) { sorting in
VStack(alignment: .leading, spacing: 14) {
Button (action: {
sortingValues.sorting = sorting
}, label: {
HStack { // 🟦
Text(sorting)
Spacer()
if sorting == sortingValue { // < this is where I add the checkmark
Image(systemName: "checkmark")
}
}
.foregroundColor(.primary)
})
if sorting != "Highest Price" {
Divider()
}
}
}
}
}
.height(.fixed(325))
// Text("Inside VStack, outside HalfASheet") // adding this Text DOES NOT make the HStack have a checkmark
Text("Inside VStack, outside HalfASheet: \(sortingValue)") // 🟨 adding this Text DOES make the HStack have a checkmark
}
.onReceive(sortingValues.$sorting) { sorting in
// the two printing lines below print correctly every time I tap the Button
print("This is SortingView. sorting:", sorting)
print("sortingValues.sorting: \(sortingValues.sorting)")
sortingValue = sorting
}
}
}
Your SortingView and ProjectSorting both access an environment object of type SortingValues, but you're passing new, separate instances to each. So the change you make in one place isn't being reflected in the other, because each view is communicating with one of two completely different objects of the same type.
If you want them to interact with the same object instance, you need to declare it at a point that's above both in the object hierarchy and make sure that that single instance is passed into both. For example:
struct ProjectsView: View {
#Binding var isPresented: Bool
#State var showSortingSheet = false
#StateObject var sortingValues = SortingValues()
var body: some View {
ZStack {
NavigationView {
VStack(spacing: 0) {
ProjectsTopView(isPresented: $isPresented)
ProjectSorting(showSortingSheet: $showSortingSheet)
.environmentObject(sortingValue)
ProjectList()
}
.navigationBarHidden(true)
}
SortingView(showSortingSheet: $showSortingSheet)
.environmentObject(sortingValues)
}
}
}
But you can go one step further. Because environment objects and values propagate down the view hierarchy automatically, you can replace two separate .environmentObject calls with one:
struct ProjectsView: View {
#Binding var isPresented: Bool
#State var showSortingSheet = false
#StateObject var sortingValues = SortingValues()
var body: some View {
ZStack {
NavigationView {
VStack(spacing: 0) {
ProjectsTopView(isPresented: $isPresented)
ProjectSorting(showSortingSheet: $showSortingSheet)
ProjectList()
}
.navigationBarHidden(true)
}
SortingView(showSortingSheet: $showSortingSheet)
}
.environmentObject(sortingValues)
}
}
There are probably better ways of dealing with reacting to changes in your observed model rather than duplicating variable values in a local state variable -- but ensuring that all your views are using the same shared environment object should get you on your way.

The data I pass to another view is not working

I'm new to swiftUI here and I want to try out to pass data between two views. But it doesn't seem to work.
I'm using Xcode 13.2 & iOS 15 for the simulator.
This is my code for the first view:
struct ContentView: View {
#State var myName: String = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter your name", text: $myName)
Text(self.myName)
NavigationLink(destination: BView(myName: self.$myName), label: {
Image(systemName: "arrowshape.turn.up.left")
})
}//: VSTACK
.padding(.horizontal, 20)
}//:NAVIGATION VIEW
}
}
This is code for the second view:
struct BView: View {
#Binding var myName: String
var body: some View {
NavigationView {
Text("BView")
Text(self.myName)
}//:NAVIGATION VIEW
}
}
I want myName to be input in the first page which is ContentView() and then pass down the input data to BView().
Unfortunately, once I run it on the simulator, the input data doesn't;t show up.
Your code is fine just add VStack in BView.
struct BView: View {
#Binding var myName: String
var body: some View {
NavigationView {
VStack { // HERE
Text("BView")
Text(self.myName)
}
}//:NAVIGATION VIEW
}
}
Please use #EnvironmentObject to pass the data to view.
https://developer.apple.com/documentation/swiftui/environmentobject

How to create a view that works with #Binding. It should refresh itself while working with #Binding data

This question is identical to SwiftUI #Binding update doesn't refresh view, but the accepted answer is not applicable for my case.
The accepted answer says
A View using a #Binding will update when the underlying #State change, but the #State must be defined within the view hierarchy. (Else you could bind to a publisher)
In my case, the view hierarchy doesn't have the view which is having the #State. The view having the binding is presented modally to the user.
To summarize the issue again
I want to create a view, similar to Toggle which initializes from a Binding. This view will show the contents from the wrapped value and as it performs the updates, the original storage of the value will get updated automatically.
As I have learnt, updating the #Binding in a view, doesn't invalidate it. Then how to implement such a view.
Also I can't depend on the parent view to eventually update this view, because the view is shown on a modally presented screen.
I don't want to use workarounds like using a #State to explicitly trigger a refresh. So what is the correct way to implement such a view.
Code example
The view TextModifier takes a Binding. The view does some modifications to the view. For now it just appends "_Updated" to the value passed.
I initialize the view as TextModifier(text: <some_binding_var>)
struct TextModifier: View {
#Binding var text: String
var body: some View {
Text(text)
.onTapGesture {
text += "_Updated"
}
}
}
This view shows the text and on tapping it updates it in the original source, but as expected the view doesn't update itself on tapping.
So, how to implement this view so that it also updates itself when it updates the binding value.
The accepted answer to the linked question also says
Else you could bind to a publisher
I don't know how to do this. Does anybody know how to implement this and also provide a code example. Thanks.
Updated with full code and gif
struct ContentView: View {
#ObservedObject var viewModel = TestViewModel()
var body: some View {
List {
ForEach(viewModel.itemsList, id: \.self) { item in
ItemView(text: $viewModel.itemsList[getItemIndex(item)])
}
}
}
private func getItemIndex(_ item: String) -> Int {
viewModel.itemsList.firstIndex { $0 == item }!
}
}
class TestViewModel: ObservableObject {
#Published var itemsList = ["Item 1", "Item 2", "Item 3"]
}
struct ItemView: View {
#Binding var text: String
#State private var showEditorView = false
var body: some View {
Text(text)
.onTapGesture {
showEditorView = true
}
.sheet(isPresented: $showEditorView) {
TextModifier(text: $text, showView: $showEditorView)
}
}
}
struct TextModifier: View {
#Binding var text: String
#Binding var showView: Bool
var body: some View {
VStack(spacing: 20) {
Text("Tap on the text to update it")
.foregroundColor(.blue)
Text(text)
.onTapGesture {
text += "_Updated"
}
Button {
showView = false
} label: {
Text("Dismiss")
.foregroundColor(.blue)
}
}
}
}

SwiftUI pass by reference a class object to another view

I'm trying to learn SwiftUI and I'm going to develop a simple app with tab views and sharing core motion data between those views.
The main idea is to create a motion manager object (like here) and use the sensor values in all views.
ContentView.swift:
import SwiftUI
struct ContentView: View {
#State private var selection = 1
#State private var viewNames : [String] = ["View1", "View2"]
var body: some View {
TabView(selection: $selection){
View1(viewName: $viewNames[0]).tag(0)
View2(viewName: $viewNames[1]).tag(1)
}
}
}
View1.swift:
import SwiftUI
struct View1 : View {
#Binding var viewName : String
var body: some View {
Text("First View")
.font(.title)
.foregroundColor(Color.gray)
.tabItem {
VStack {
Image(systemName: "star")
Text(viewName)
}
}
}
}
View2.swift:
struct View2 : View {
#Binding var viewName : String
var body: some View {
VStack {
Text("Second View")
.font(.title)
.foregroundColor(Color.green)
.padding(.top)
View21(motionManager: MotionManager())
}.tabItem {
VStack {
Image(systemName:"heart")
Text(viewName)
}
}
}
}
View21.swift
struct View21 : View {
#ObservedObject var motionManager : MotionManager
#State private var showDetails = false
var body: some View{
Text(String(format: "%.2f", motionManager.x))
}
With these code I can use the sensor data in View21, but I can't access the data in the views in the hierarchy above.
Furthermore I created #ObservedObject in the ContentView (like here) and passed it through all views. My app work in the simulator, but it doesn't on the real device.
I can see sensor data changing, but I can't switch the tab views.
I've tried to use #EnvironementObject instead of #ObservedObject, but the behavior is the same.
I'll be very thankful for any help and tipps to my issue.
Best wishes
Okay, so Paulw11 is right that the you probably want to inject your ObservableObject into your environment, then in each view that wants to access that instance, you just add a property with the #EnvironmentObject property wrapper. Below I've thrown together the simplest example I could think of, so that you can get the idea of how it works.
import SwiftUI
import Combine
class ManagerPlaceholder: ObservableObject {
#Published var propertyOne: Double = 1.0
#Published var propertyTwo: Double = 2.0
func action() {
propertyOne = Double.random(in: 0.0..<100.00)
propertyTwo = Double.random(in: 0.0..<100.00)
}
}
struct ContentView: View {
#EnvironmentObject var manager: ManagerPlaceholder
var body: some View {
TabView {
Subview()
.tabItem { Label("First", systemImage: "hexagon.fill") }
.tag(1)
Subview()
.tabItem { Label("Second", systemImage: "circle.fill") }
.tag(2)
}
}
}
struct Subview: View {
#EnvironmentObject var manager: ManagerPlaceholder
var body: some View {
VStack {
Text("Prop One: \(manager.propertyOne)").padding()
Text("Prop Two: \(manager.propertyTwo)").padding()
Button("Change", action: manager.action).padding()
}
}
}
So above is
A simple ObservableObject - All it does is set two Doubles with a random value (notice the properties you want to observe are marked as #Published)
A tab view
A simple sub-view
Notice that both of the views have a #EnvironmentObject var manager: ManagerPlaceholder. You don't set that property directly. That line says that you want to reference a ManagerPlaceholder instance that's in the Environment. The environment is a sort of "pool" of storage that SwiftUI manages for you. You can add an instance of an object to it, then reference it in sub-views that need it.
So to make sure that's in the environment you add it when instantiating a view (could be the tab view, or any super-view). So for example in your _NAME_App.swift (for an iOS 14 target) or SceneDelegate.swift (for an iOS 13 target), you'd instantiate your view like this:
ContentView().environmentObject(ManagerPlaceholder())
If you run the code above you'll see when you hit the Change button it randomly sets the two properties, and both subviews will see the exact same values when you switch back and forth, because they're both referencing the same instance.
Feel free to comment if anything is unclear.

SwiftUI #Binding update doesn't refresh view

I feel like I'm missing something very basic, but this example SwiftUI code will not modify the view (despite the Binding updating) when the button is clicked
Tutorials I have read suggest this is the correct way to use a binding and the view should refresh automatically
import SwiftUI
struct ContentView: View {
#Binding var isSelected: Bool
var body: some View {
Button(action: {
self.isSelected.toggle()
}) {
Text(isSelected ? "Selected" : "Not Selected")
}
}
}
struct ContentView_Previews: PreviewProvider {
#State static var selected: Bool = false
static var previews: some View {
ContentView(isSelected: $selected)
}
}
You have not misunderstood anything. A View using a #Binding will update when the underlying #State change, but the #State must be defined within the view hierarchy. (Else you could bind to a publisher)
Below, I have changed the name of your ContentView to OriginalContentView and then I have defined the #State in the new ContentView that contains your original content view.
import SwiftUI
struct OriginalContentView: View {
#Binding var isSelected: Bool
var body: some View {
Button(action: {
self.isSelected.toggle()
}) {
Text(isSelected ? "Selected" : "Not Selected")
}
}
}
struct ContentView: View {
#State private var selected = false
var body: some View {
OriginalContentView(isSelected: $selected)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
SwiftUI View affects #Binding. #State affects SwiftUI View.
#State var affects the view, but to affect another #State it must be used as binding by adding leading $ to value name and it works only inside SwiftUI.
To trigger SwiftUI change from outside, i.e. to deliver/update Image, use Publisher that looks like this:
// Declare publisher in Swift (outside SwiftUI).
public let imagePublisher = PassthroughSubject<Image, Never>()
// It must be handled within SwiftUI.
struct ContentView: View {
// Declare a #State that updates View.
#State var image: Image = Image(systemName: "photo")
var body: some View {
// Use #State image declaration
// and subscribe this value to publisher "imagePublisher".
image.onReceive(imagePublisher, perform: { (output: Image) in
self.image = output // Whenever publisher sends new value, old one to be replaced
})
}
}
// And this is how to send value to update SwiftUI from Swift:
imagePublisher.send(Image(systemName: "photo"))
In the top Level of SwiftUI, #Binding cannot refresh View hierarchy unless manually adding a #state or other refreshing triggers.
struct ContentView: View {
#Binding var isSelected : Bool
#State var hiddenTrigger = false
var body: some View {
VStack {
Text("\(hiddenTrigger ? "" : "")")
Button(action: {
self.isSelected.toggle()
self.hiddenTrigger = self.isSelected
}) {
Text(self.isSelected? "Selected" : "not Selected")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var selected: Bool = false
static var previews: some View {
ContentView(isSelected: Binding<Bool>(get: {selected}, set: { newValue in
selected = newValue}))
}
}
Looking into this some more I think I understand what's happening.
In this instance I want to use #Binding as I'm building a custom control (like SwiftUI's native Toggle, which also binds to a Bool)
The issue is that the static state in ContentView_Previews (i.e., the line #State static var selected: Bool = false) does not trigger a re-render of the preview when the state changes, so even though the selected state has changed due to interaction with the control, the control (a child of ContentView_Previews) does not re-render itself
This makes it tough to test controls in isolation in the SwiftUI preview, however moving the state into a dummy ObservableObject instance functions correctly. Here's the code:
import SwiftUI
import Combine
class SomeData: ObservableObject {
#Published var isOn: Bool = false
}
struct MyButton: View {
#Binding var isSelected: Bool
var body: some View {
Button(action: {
self.isSelected.toggle()
}) {
Text(isSelected ? "Selected" : "Not Selected")
}
}
}
struct ContentView: View {
#EnvironmentObject var data: SomeData
var body: some View {
MyButton(isSelected: $data.isOn)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(SomeData())
}
}
It seems that a change in #State static var doesn't trigger a preview re-render. In the above code my #Binding example is moved into MyButton and the content view's dummy environment instance is bounds to its isSelected property. Tapping the button updates the view as expected in the SwiftUI preview.
You need to use #State instead of #Binding.
If the UI should update when its value changes, you designate a variable as a
#State variable. It is the source of truth.
You use #Binding instead of #State, when the view doesn't own this data and its not the source of truth.
Here is your variable:
#State var isSelected: Bool
In my case, having the #Binding or #State control a top level if statement caused issues.
I put the if check inside a top level VStack and it started working fine.
struct ContentView: View {
#Binding var value: Bool // #State breaks too
var body: some View {
// Add a VStack here to fix the bug
if value { // Top level `if` based on #State or #Binding won't work
Text("view 1")
} else {
Text("view 2")
.onAppear {
value = true // Won't trigger update
}
}
}
}
This was only sometimes though.. Depending on what the rest of the view hierarchy looked like. My view hierarchy was nested inside a NavigationView, a TabView, a ZStack, etc. I'm not sure what the minimum requirements are to trigger this. Really weird behavior.

Resources