I have an environment value as follows:
#Environment(\.isPresented) var isPresented: Bool
This environment value is defined as such:
private struct IsPresented: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var isPresented: Bool {
get { self[IsPresented.self] }
set { self[IsPresented.self] = newValue }
}
}
extension View {
func isPresented(_ isPresented: Bool) -> some View {
environment(\.isPresented, isPresented)
}
}
I want to read this environment value in one of my views to decide whether or not to show a view as a full screen. However, this line of code doesn't work:
.fullScreenCover(isPresented: self.$isPresented) {
// It says there's no such member in my view.
Thus, my question is, how can I convert my environment value to a binding, since .fullScreenCover expects a binding?
If you want it to be a writable value, I think your Environment value should be Binding<Bool> instead of just Bool. This is how the system's presentationMode works, for example.
private struct IsPresented: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var isPresented: Binding<Bool> {
get { self[IsPresented.self] }
set { self[IsPresented.self] = newValue }
}
}
extension View {
func isPresented(_ isPresented: Binding<Bool>) -> some View {
environment(\.isPresented, isPresented)
}
}
struct ContentView : View {
#State private var isPresented = false
var body: some View {
ChildView().environment(\.isPresented, $isPresented)
}
}
struct ChildView : View {
#Environment(\.isPresented) var isPresented: Binding<Bool>
var body: some View {
Button("Test") {
isPresented.wrappedValue = true
}
.fullScreenCover(isPresented: isPresented) {
Text("Sheet")
}
}
}
If you really want #Environment(\.isPresented) var isPresented: Bool as Bool, then you can create Binding in place, like
.fullScreenCover(isPresented: Binding(
get: { isPresented },
set: { isPresented = $0 }
)) {
Related
In SwiftUI I've created a struct that should create different overlay views depending on some state variables. If any of the state booleans is true, then it should return custom view (either ErrorOverlay or LoadingOverlay or else an EmptyView) like this:
struct OverlayContainer: View {
#State var isLoading: Bool = false
#State var isErrorShown: Bool = false
func setIsLoading(isLoading: Bool) {
self.isLoading = isLoading
}
func setIsErrorShown(isErrorShown: Bool) {
self.isErrorShown = isErrorShown
}
var body: some View {
Group {
if(isErrorShown) {
ErrorOverlay()
}
else if(isLoading) {
LoadingOverlay()
}
else {
EmptyView()
}
}
}
}
Now I've implemented the overlay on some content in the Home view with buttons that should change the state and show the correct overlay, like this:
struct Home: View {
var body: some View {
let overlayContainer = OverlayContainer()
return HStack {
// Some more content here
Button(action: {
overlayContainer.setIsLoading(isLoading: true)
}) {
Text("Start loading")
}
Button(action: {
overlayContainer.setIsErrorShown(isErrorShown: true)
}) {
Text("Show error")
}
}.overlay(overlayContainer)
}
}
This isn't working: when I click the button nothing happens. Why and how to solve this? (without using binding, see below)
ps. I've been able to get a working solution by doing the following:
extracting the state booleans to the Home view
pass these through the constructor of the OverlayContainer
change the state booleans instead of calling the set methods when clicking the buttons
change the OverlayContainer so it implements an init method with both booleans
change the state booleans in the OverlayContainer to bindings.
However, I'd like to implement the states in the OverlayContainer to be able to re-use that in different screens, without implementing state variables in all of these screens. Firstly because there will probably be more cases than just these 2. Secondly because not all screens will need to access all states and I haven't found out a simple way to implement optional bindings through the init method.
To me it feels that all these states belong to the OverlayContainer, and changing the state should be as short and clean as possible. Defining states everywhere feels like code duplication. Maybe I need a completely different architecture?
It should be used Binding instead. Here is possible solution.
struct OverlayContainer: View {
#Binding var isLoading: Bool
#Binding var isErrorShown: Bool
var body: some View {
Group {
if(isErrorShown) {
ErrorOverlay()
}
else if(isLoading) {
LoadingOverlay()
}
else {
EmptyView()
}
}
}
}
struct Home: View {
#State var isLoading: Bool = false
#State var isErrorShown: Bool = false
var body: some View {
HStack {
// Some more content here
Button(action: {
self.isLoading = true
}) {
Text("Start loading")
}
Button(action: {
self.isErrorShown = true
}) {
Text("Show error")
}
}.overlay(OverlayContainer(isLoading: $isLoading, isErrorShown: $isErrorShown))
}
}
To make it the way you want, use Binding:
struct OverlayContainer: View {
#Binding var isLoading: Bool
#Binding var isErrorShown: Bool
func setIsLoading(isLoading: Bool) {
self.isLoading = isLoading
self.isErrorShown = !isLoading
}
func setIsErrorShown(isErrorShown: Bool) {
self.isErrorShown = isErrorShown
self.isLoading = !isErrorShown
}
var body: some View {
Group {
if(isErrorShown) {
ErrorOverlay()
}
else if(isLoading) {
LoadingOverlay()
}
else {
EmptyView()
}
}
}
}
struct Home: View {
#State var isLoading = false
#State var isErrorShown = false
var body: some View {
let overlayContainer = OverlayContainer(isLoading: $isLoading, isErrorShown: $isErrorShown)
return HStack {
// Some more content here
Button(action: {
overlayContainer.setIsLoading(isLoading: true)
}) {
Text("Start loading")
}
Button(action: {
overlayContainer.setIsErrorShown(isErrorShown: true)
}) {
Text("Show error")
}
}.overlay(overlayContainer)
}
}
I created a custom button, that shows a popover. Here is my code:
PopupPicker
struct PopupPicker: View {
#State var selectedRow: UUID?
#State private var showPopover = false
let elements: [PopupElement]
var body: some View {
Button((selectedRow != nil) ? (elements.first { $0.id == selectedRow! }!.text) : elements[0].text) {
self.showPopover = true
}
.popover(isPresented: self.$showPopover) {
PopupSelectionView(elements: self.elements, selectedRow: self.$selectedRow)
}
}
}
PopupSelectionView
struct PopupSelectionView: View {
var elements: [PopupElement]
#Binding var selectedRow: UUID?
var body: some View {
List {
ForEach(self.elements) { element in
PopupText(element: element, selectedRow: self.$selectedRow)
}
}
}
}
PopupText
struct PopupText: View {
var element: PopupElement
#Binding var selectedRow: UUID?
var body: some View {
Button(element.text) {
self.presentation.wrappedValue.dismiss()
self.selectedRow = self.element.id
}
}
}
That works fine, but can I create a custom event modifier, so that I can write:
PopupPicker(...)
.onSelection { popupElement in
...
}
I can't give you a full solution as I don't have all of your code and thus your methods to get the selected item anyhow, however I do know where to start.
As it turns out, declaring a function with the following syntax:
func `onSelection`'(arg:type) {
...
}
Creates the functionality of a .onSelection like so:
struct PopupPicker: View {
#Binding var selectedRow: PopupElement?
var body: some View {
...
}
func `onSelection`(task: (_ selectedRow: PopupElement) -> Void) -> some View {
print("on")
if self.selectedRow != nil {
task(selectedRow.self as! PopupElement)
return AnyView(self)
}
return AnyView(self)
}
}
You could theoretically use this in a view like so:
struct ContentView: View {
#State var popupEl:PopupElement?
var body: some View {
PopupPicker(selectedRow: $popupEl)
.onSelection { element in
print(element.name)
}
}
}
However I couldn't test it properly, please comment on your findings
Hope this could give you some insight in the workings of this, sorry if I couldn't give a full solution
I'm building a UI component with SwiftUI that should have trigger from outside to turn on animation and some inner preparations for it. In examples below it's prepareArray() function.
My first approach was to use bindings, but I've found that there is no way to listen when #Binding var changes to trigger something:
struct ParentView: View {
#State private var animated: Bool = false
var body: some View {
VStack {
TestView(animated: $animated)
Spacer()
Button(action: {
self.animated.toggle()
}) {
Text("Toggle")
}
Spacer()
}
}
}
struct TestView: View {
#State private var array = [Int]()
#Binding var animated: Bool {
didSet {
prepareArray()
}
}
var body: some View {
Text("\(array.count): \(animated ? "Y" : "N")").background(animated ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
}
private func prepareArray() {
array = [1]
}
}
Why then it allows didSet listener for #Binding var if it's not working?! Then I switched to simple Combine signal since it's can be caught in onReceive closure. But #State on signal was not invalidating view on value pass:
struct ParentView: View {
#State private var animatedSignal = CurrentValueSubject<Bool, Never>(false)
var body: some View {
VStack {
TestView(animated: animatedSignal)
Spacer()
Button(action: {
self.animatedSignal.send(!self.animatedSignal.value)
}) {
Text("Toggle")
}
Spacer()
}
}
}
struct TestView: View {
#State private var array = [Int]()
#State var animated: CurrentValueSubject<Bool, Never>
var body: some View {
Text("\(array.count): \(animated.value ? "Y" : "N")").background(animated.value ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1)).onReceive(animated) { animated in
if animated {
self.prepareArray()
}
}
}
private func prepareArray() {
array = [1]
}
}
So my final approach was to trigger inner state var on signal value:
struct ParentView: View {
#State private var animatedSignal = CurrentValueSubject<Bool, Never>(false)
var body: some View {
VStack {
TestView(animated: animatedSignal)
Spacer()
Button(action: {
self.animatedSignal.send(!self.animatedSignal.value)
}) {
Text("Toggle")
}
Spacer()
}
}
}
struct TestView: View {
#State private var array = [Int]()
let animated: CurrentValueSubject<Bool, Never>
#State private var animatedInnerState: Bool = false {
didSet {
if animatedInnerState {
self.prepareArray()
}
}
}
var body: some View {
Text("\(array.count): \(animatedInnerState ? "Y" : "N")").background(animatedInnerState ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1)).onReceive(animated) { animated in
self.animatedInnerState = animated
}
}
private func prepareArray() {
array = [1]
}
}
Which works fine, but I can't believe such a simple task requires so complicated construct! I know that SwiftUI is declarative, but may be I'm missing more simple approach for this task? Actually in real code this animated trigger will have to be passed to one more level deeper(
It is possible to achieve in many ways, including those you tried. Which one to choose might depend on real project needs. (All tested & works Xcode 11.3).
Variant 1: modified your first try with #Binding. Changed only TestView.
struct TestView: View {
#State private var array = [Int]()
#Binding var animated: Bool
private var myAnimated: Binding<Bool> { // internal proxy binding
Binding<Bool>(
get: { // called whenever external binding changed
self.prepareArray(for: self.animated)
return self.animated
},
set: { _ in } // here not used, so just stub
)
}
var body: some View {
Text("\(array.count): \(myAnimated.wrappedValue ? "Y" : "N")")
.background(myAnimated.wrappedValue ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
}
private func prepareArray(for animating: Bool) {
DispatchQueue.main.async { // << avoid "Modifying state during update..."
self.array = animating ? [1] : [Int]() // just example
}
}
}
Variant2 (my preferable): based on view model & publishing, but requires changes both ParentView and TestView, however in general simpler & clear.
class ParentViewModel: ObservableObject {
#Published var animated: Bool = false
}
struct ParentView: View {
#ObservedObject var vm = ParentViewModel()
var body: some View {
VStack {
TestView()
.environmentObject(vm) // alternate might be via argument
Spacer()
Button(action: {
self.vm.animated.toggle()
}) {
Text("Toggle")
}
Spacer()
}
}
}
struct TestView: View {
#EnvironmentObject var parentModel: ParentViewModel
#State private var array = [Int]()
var body: some View {
Text("\(array.count): \(parentModel.animated ? "Y" : "N")")
.background(parentModel.animated ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
.onReceive(parentModel.$animated) {
self.prepareArray(for: $0)
}
}
private func prepareArray(for animating: Bool) {
self.array = animating ? [1] : [Int]() // just example
}
}
Is it possible to depend on multiple conditions in SwiftUI? For example to show a sheet:
.sheet(isPresented: $stateA && $stateB, content: { ... }) // this is not working
Or is a different approach known?
no, it is not possible! isPresented accept Binding, that means the state is updated if sheet will be dismissed. Which of stateA, stateB have to be changed? or both of them? Even though someone will try to define && operator where left and right side is Binding, that is very bad idea. Don't try to do it!
Move the logic to your model, better outside of any View.
UPDATE (for Asperi)
this is valid code (with your extension)
struct ContentView: View {
#State private var isFirst = true
#State private var isSecond = false
var body: some View {
VStack {
Button("TestIt") {
self.isSecond = true
}
.sheet(isPresented: $isFirst && $isSecond) {
Text("A")
}
}
}
}
Try it! Pressing TestIt will open the sheet. There is no Button to "go back", but you can dismiss it with well known gesture. And try to press TestIt again ...
"I can only show you the door..." (c) Morpheus
Today is a day of overloaded operators :^) - previous was here, here is for your case (tested with Xcode 11.3+)
extension Binding where Value == Bool {
static func &&(_ lhs: Binding<Bool>, _ rhs: Binding<Bool>) -> Binding<Bool> {
return Binding<Bool>( get: { lhs.wrappedValue && rhs.wrappedValue },
set: {_ in })
}
}
struct TestCustomBinding: View {
#State private var isFirst = true
#State private var isSecond = false
var body: some View {
VStack {
Button("TestIt") {
self.isSecond = true
}
.sheet(isPresented: $isFirst && $isSecond) {
Button("CloseMe") {
// sheet MUST be closed explicitly via one of states !
self.isSecond = false
}
}
}
}
}
It is possible to get different conditions from a variable.
struct ChangingButton: View {
var text: String
var onButton: String
var offButton: String
var changeButton: Bool
var buttonCondition: String {
if isOn {
return isOnImage
} else {
return isOffImage
}
}
var body: some View {
Button(action: {
action()
}
, label: {
VStack {
Image(systemName: buttonCondition)
Text(text)
}
})
}
}
struct ChangingButton_Previews: PreviewProvider {
static var previews: some View {
ChangingButton(text: "My Button", onButton: "on", offButton: "off", changeButton: true, action: {
}).background(Color.black)
}
Im trying to create an environment object that is editable and putting it in a list.
The Variables are only refreshing when I switch the tab for example (so whenever I leave the NavigationView) and then come back.
The same worked with a ModalView before. Is it a bug maybe? Or am I doing something wrong?
import SwiftUI
import Combine
struct TestView: View {
#State var showSheet: Bool = false
#EnvironmentObject var feed: TestObject
func addObjects() {
var strings = ["one","two","three","four","five","six"]
for s in strings {
var testItem = TestItem(text: s)
self.feed.items.append(testItem)
}
}
var body: some View {
TabView {
NavigationView {
List(feed.items.indices, id:\.self) { i in
NavigationLink(destination: detailView(feed: self._feed, i: i)) {
HStack {
Text(self.feed.items[i].text)
Text("(\(self.feed.items[i].read.description))")
}
}
}
}
.tabItem({ Text("Test") })
.tag(0)
Text("Blank")
.tabItem({ Text("Test") })
.tag(0)
}.onAppear {
self.addObjects()
}
}
}
struct detailView: View {
#EnvironmentObject var feed: TestObject
var i: Int
var body: some View {
VStack {
Text(feed.items[i].text)
Text(feed.items[i].read.description)
Button(action: { self.feed.items[self.i].isRead.toggle() }) {
Text("Toggle read")
}
}
}
}
final class TestItem: ObservableObject {
init(text: String) {
self.text = text
self.isRead = false
}
static func == (lhs: TestItem, rhs: TestItem) -> Bool {
lhs.text < rhs.text
}
var text: String
var isRead: Bool
let willChange = PassthroughSubject<TestItem, Never>()
var read: Bool {
set {
self.isRead = newValue
}
get {
self.isRead
}
}
}
class TestObject: ObservableObject {
var willChange = PassthroughSubject<TestObject, Never>()
#Published var items: [TestItem] = [] {
didSet {
willChange.send(self)
}
}
}
try passing .environmentObject on your destination:
NavigationLink(destination: detailView(feed: self._feed, i: i).environmentObject(x))
You have to use willSet instead of didSet.
TestItem should be a value type: struct or enum. SwiftUI's observation system properly works only with value types.