ignoresSafeArea not covering bottom of View - swiftui

I'm trying to create the background for a background view for a popup, however the view doesn't cover the bottom even after I use .ignoreSafeArea().
import SwiftUI
struct Overlay<Content: View>: View {
#State var opacity: Double = 0
var content: Content
var body: some View {
VStack {
Color.black.opacity(opacity).ignoresSafeArea()
content
}
.onAppear { self.opacity = 0.5 }
.onDisappear { self.opacity = 0 }
}
}
struct ForgotPasswordView_Previews: PreviewProvider {
static var previews: some View {
Group {
Overlay(content: Text(""))
.previewDevice("iPhone 12")
}
}
}

VStack arranges its children in a vertical line.
What You see in the bottom area is your content which you pass in the view creation (here its your TextView).
struct Overlay<Content: View>: View {
#State var opacity: Double = 0
var content: Content
var body: some View {
ZStack { //<- here
Color.black.opacity(opacity).ignoresSafeArea()
content
}
.onAppear { self.opacity = 0.5 }
.onDisappear { self.opacity = 0 }
}
}

this is the solution i was able to come up with:
import SwiftUI
struct Overlay<Content: View>: View {
#State var opacity: Double = 1
var content: Content
var body: some View {
VStack {
Color.black
.opacity(0)
content
}
.background(Color.black.opacity(opacity))
.ignoresSafeArea(.all)
.onAppear { self.opacity = 0.5 }
.onDisappear { self.opacity = 0 }
}
}
struct ForgotPasswordView_Previews: PreviewProvider {
static var previews: some View {
Group {
Overlay(content: Text("hi"))
.previewDevice("iPhone 12")
}
}
}
Here is an image of it working
if you want the content to be overlayed on top of the view you can use a ZStack with the color in the back and the content in the front. this can be implemented by removeing the V in VStack and replacing it with a Z. Beutiful image of it working!

Related

How I can add a drawing function in SwiftUI?

I can't draw. Unfortunately I don't know where the problem is, colour selection works and the canva is also created, only the drawing doesn't work. I have also checked that the size of the canva is correct and that the path is defined correctly. here is my code:
//
import SwiftUI
struct ContentView: View {
#State private var lines: [Line] = []
#State private var strokeColor: Color = Color.black
var body: some View {
ZStack {
Color.white
.edgesIgnoringSafeArea(.all)
VStack {
Canvas(lines: $lines, strokeColor: $strokeColor)
HStack {
Button(action: clearDrawing) {
Text("Clear")
}
ColorPicker("Stroke Color", selection: $strokeColor)
}
.padding()
}
}
}
func clearDrawing() {
lines.removeAll()
}
}
//
Here I have created the Canva and the button to delete the drawing
//
struct Canvas: View {
#Binding var lines: [Line]
#Binding var strokeColor: Color
var body: some View {
GeometryReader { geometry in
Path { path in
for line in self.lines {
path.move(to: line.points[0])
path.addLines(line.points)
}
}
.stroke(self.strokeColor, lineWidth: 3)
.frame(width: geometry.size.width, height: geometry.size.height)
.gesture(
DragGesture(minimumDistance: 0.1)
.onChanged({ value in
var lastLine = self.lines.last!
let currentPoint = value.location
lastLine.points.append(currentPoint)
})
.onEnded({ value in
self.lines.append(Line())
})
)
}
}
}
struct Line: Identifiable {
var id: UUID = UUID()
var points: [CGPoint] = []
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//
And here I have made it so that you should actually draw by touch, and that there is a colour selection

SwiftUI gesture state is reset between gestures

I have the following code for a simple square to which I attach a MagnificationGesture to update its state with a pinch to zoom gesture.
import SwiftUI
struct ContentView2: View {
var scale: CGFloat = 1
var magnificationGesture: some Gesture {
MagnificationGesture()
.onChanged { value in
scale = value
}
}
var body: some View {
VStack {
Text("\(scale)")
Spacer()
Rectangle()
.fill(Color.red)
.scaleEffect(self.scale)
.gesture(
magnificationGesture
)
Spacer()
}
}
}
struct ContentView2_Previews: PreviewProvider {
static var previews: some View {
ContentView2()
}
}
However this simple view behaves weird. When I perform the gesture, the scale #State property is succesfully modified. However when I then do another gesture with my hands, the scale property is reset to its initial state, instead of starting from its current value.
Here is a video of what happens. For example, when the red view is very small, performing the gesture, I would expect that it stays small, instead of completely resetting.
How can I get the desired behaviour - that is - the scale property is not reset
I was able to get it working by adding a bit to the code. Check it out and let me know if this works for your use case:
import SwiftUI
struct ContentView2: View {
var magGesture = MagnificationGesture()
#State var magScale: CGFloat = 1
#State var progressingScale: CGFloat = 1
var magnificationGesture: some Gesture {
magGesture
.onChanged { progressingScale = $0 }
.onEnded {
magScale *= $0
progressingScale = 1
}
}
var body: some View {
VStack {
Text("\(magScale)")
Spacer()
Rectangle()
.fill(Color.red)
.scaleEffect(self.magScale * progressingScale)
.gesture(
magnificationGesture
)
Spacer()
}
}
}
struct ContentView2_Previews: PreviewProvider {
static var previews: some View {
ContentView2()
}
}
I solved it by adding another scale and only updating one of them at the end to keep track of the scale
Code
import SwiftUI
struct ContentView2: View {
#State var previousScale: CGFloat = 1
#State var newScale: CGFloat = 1
var magnificationGesture: some Gesture {
MagnificationGesture()
.onChanged { value in
newScale = previousScale * value
}
.onEnded { value in
previousScale *= value
}
}
var body: some View {
VStack {
Text("\(newScale)")
Spacer()
Rectangle()
.fill(Color.red)
.scaleEffect(newScale)
.gesture(
magnificationGesture
)
Spacer()
}
}
}
struct ContentView2_Previews: PreviewProvider {
static var previews: some View {
ContentView2()
}
}

SwiftUI: withAnimation not working with App #State

I have a #State var opacity at the App level (i.e. not in a View). I would like to animate its changes. The changes do get applied (causing re-render), but it doesn't animate. I could only get animation to work when I moved that state var into the view.
Is there a way to get withAnimation to work with App state?
My reason for storing that state at the App level is, I also want to pass it to the Settings scene.
This does not work:
#main
struct MacosPlaygroundApp: App {
#State private var opacity: Double = 1.0
var body: some Scene {
WindowGroup {
ContentView(opacity: opacity, onClick: {
withAnimation(.linear(duration: 1)) {
opacity -= 0.5
}
})
}
}
}
struct ContentView: View {
let opacity: Double
let onClick: () -> Void
var body: some View {
Button("Press here") {
onClick()
}
.padding()
.opacity(opacity)
}
}
This works:
The opacity-change animates for both the button in ContentView and the shape in ChildView.
#main
struct MacosPlaygroundApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State private var opacity: Double = 1.0
var body: some View {
VStack {
Button("Press here") {
withAnimation(.linear(duration: 1)) {
opacity -= 0.5
}
}
ChildView(opacity: opacity)
}
.padding()
.opacity(opacity)
}
}
struct ChildView: View {
let opacity: Double
var body: some View {
Color.pink
.contentShape(Rectangle())
.opacity(opacity)
.frame(width: 200, height: 200)
}
}

ContentView redraw from change in modalView not triggered

Ever since the advent of swiftUI 2.0, I have been unable to update a view according to a change done in another modally-presented view (the settings view).
I display a string on my main ContentView that derives its content from a segmented Picker value on the SettingsView.
The problem is that after the user changes the setting and discards the SettingsView, the string in ContentView is not updated. The body is not redrawn.
I am making use of #ObservableObject and #StateObject so every change to it should trigger a redraw, but I can't make it work...
I created a class that conforms to the ObservableObject protocol : AppState
I am using that class to try and pass data and -more importantly- data changes between the views in order to have my ContentView redrawn according to the the user's settings.
In order to instantiate this class, I registered a single UserDefaults in my AppDelegate file.
I also imported the Combine Framework into my project and added the import Combine line in each and every file !
I've simplified my code as much as possible, in order to illustrate the issue, so the following might seem a bit circumvolutated, but it is derived from a much more complex app, sorry about that.
Here is my ContentView code :
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var appState: AppState
#State var modalViewCaller = 0 // used to present correct modalView
#State var modalIsPresented = false // to present the modal views
var body: some View {
let stringArray = generateString() // func to generate string according to user's pref
let recapString = stringArray[0]
return ZStack {
NavigationView {
VStack {
// MARK: - texts :
VStack {
Text(recapString)
.bold()
.multilineTextAlignment(/*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
} // end of VStack
.padding()
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color(UIColor.systemBlue), lineWidth: 4))
.padding()
} // END of VStack
.onAppear() {
self.modalViewCaller = 0
print("\n\n*********** Content View onAppear triggered ! ************\n")
}
.navigationBarTitle("DataFun", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
self.modalViewCaller = 1 // SettingsView
self.modalIsPresented = true
}
) {
Image(systemName: "gear")
.imageScale(.large)
}
))
} // END of NavigationView
.onAppear() {
self.appState.updateValues()
}
} // End of ZStack
.sheet(isPresented: $modalIsPresented) {
sheetContent(modalViewCaller: $modalViewCaller, appState: AppState())
}
.navigationViewStyle(StackNavigationViewStyle())
}
// MARK: - struct sheetContent() :
struct sheetContent: View {
#Binding var modalViewCaller: Int // Binding to the #State modalViewCaller variable from ContentView
#StateObject var appState: AppState
var body: some View {
if modalViewCaller == 1 { // The settings view is called
SettingsView(appState: AppState())
.navigationViewStyle(StackNavigationViewStyle())
.onDisappear { self.modalViewCaller = 0 }
} else if modalViewCaller == 2 { // the "other view" is called
OtherView()
.navigationViewStyle(StackNavigationViewStyle())
.onDisappear { self.modalViewCaller = 0 }
}
}
} // END of func sheetContent
// MARK: - generateString()
func generateString() -> [String] {
var recapString = "" // The recap string
var myArray = [""]
// We create the recap string :
if UserDefaults.standard.integer(forKey: "rules selection") == 0 { // ICAO
recapString = "User chose LEFT"
} else if UserDefaults.standard.integer(forKey: "rules selection") == 1 { // AF Rules
recapString = "User chose RIGHT"
}
myArray = [recapString]
return myArray
} // End of func generateString()
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(appState: AppState())
}
}
Here is my AppState code :
import Foundation
import SwiftUI
import Combine
class AppState: ObservableObject {
#Published var rulesSelection: Int = UserDefaults.standard.integer(forKey: "rules selection")
func updateValues() { // When the user changes a setting, the UserDefault is updated. Here, we align the AppState's value with what is now in the UserDefaults
self.rulesSelection = UserDefaults.standard.integer(forKey: "rules selection")
print("\nappState value (ruleSelection) updated from Appstate class func \"updateValues")
}
}
Here is my SettingsView code :
import SwiftUI
import Combine
struct SettingsView: View {
#Environment(\.presentationMode) var presentationMode // in order to dismiss the Sheet
#StateObject var appState: AppState
#State private var rulesSelection = UserDefaults.standard.integer(forKey: "rules selection") // 0 is LEFT, 1 is RIGHT
var body: some View {
NavigationView {
VStack {
Spacer()
Text("Choose a setting below")
.padding()
Picker("", selection: $rulesSelection) {
Text("LEFT").tag(0)
Text("RIGHT").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
Spacer()
}
.navigationBarItems(
leading:
Button("Done") {
self.saveDefaults() // We set the UserDefaults
self.presentationMode.wrappedValue.dismiss() // This dismisses the view
// self.modalViewCaller = 0
}
) // END of NavBarItems
} // END of NavigationBiew
} // END of body
func saveDefaults() {
UserDefaults.standard.set(rulesSelection, forKey: "rules selection")
self.appState.updateValues() // This is a func from the AppState class that will align the appState's value to the UserDefaults
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView(appState: AppState())
}
}
And a working project if anyone has the time to check this "live" :
https://github.com/Esowes/dataFun
Thanks for any pointers.
Regards.
Well... it was... in short many changes, so here is complete ContentView.swift with fixes.
Note: you need only one StateObject, and one instance set into it, and you need to have published property of observable object in view otherwise it is not refreshed, and changes in UserDefaults do not refresh view until you use AppStorage, etc.
Verified with Xcode 12.1 / iOS 14.1
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var appState: AppState
#State var modalViewCaller = 0 // used to present correct modalView
#State var modalIsPresented = false // to present the modal views
var body: some View {
return ZStack {
NavigationView {
VStack {
// MARK: - texts :
VStack {
RecapStringView(appState: appState)
} // end of VStack
.padding()
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color(UIColor.systemBlue), lineWidth: 4))
.padding()
} // END of VStack
.onAppear() {
self.modalViewCaller = 0
print("\n\n*********** Content View onAppear triggered ! ************\n")
}
.navigationBarTitle("DataFun", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
self.modalViewCaller = 1 // SettingsView
self.modalIsPresented = true
}
) {
Image(systemName: "gear")
.imageScale(.large)
}
))
} // END of NavigationView
.onAppear() {
self.appState.updateValues()
}
} // End of ZStack
.sheet(isPresented: $modalIsPresented) {
sheetContent(modalViewCaller: $modalViewCaller, appState: appState)
}
.navigationViewStyle(StackNavigationViewStyle())
}
// MARK: - struct sheetContent() :
struct sheetContent: View {
#Binding var modalViewCaller: Int // Binding to the #State modalViewCaller variable from ContentView
#ObservedObject var appState: AppState
var body: some View {
if modalViewCaller == 1 { // The settings view is called
SettingsView(appState: appState)
.navigationViewStyle(StackNavigationViewStyle())
.onDisappear { self.modalViewCaller = 0 }
} else if modalViewCaller == 2 { // the "other view" is called
OtherView()
.navigationViewStyle(StackNavigationViewStyle())
.onDisappear { self.modalViewCaller = 0 }
}
}
} // END of func sheetContent
}
struct RecapStringView: View {
#ObservedObject var appState: AppState
var body: some View {
Text("User chose " + "\(appState.rulesSelection == 0 ? "LEFT" : "RIGHT")")
.bold()
.multilineTextAlignment(.center)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(appState: AppState())
}
}

How do you call a function from a subview in SwiftUI?

I have a child/subview which is a template for instances inside the parent view. This child view a is circle that can be moved around the screen by the user. The initial drag of this circle is supposed to call a function in the parent view, which appends a new instance of the child view. Basically, initially moving the circle from its starting position creates a new circle in that start position. Then when you initially move that new one, which was just created, another new circle is created at that starting position again, etc...
How do I call the addChild() function that's in ContentView from the Child view?
Thank you, I appreciate any help.
import SwiftUI
struct ContentView: View {
#State var childInstances: [Child] = [Child(stateBinding: .constant(.zero))]
#State var childInstanceData: [CGSize] = [.zero]
#State var childIndex = 0
func addChild() {
self.childInstanceData.append(.zero)
self.childInstances.append(Child(stateBinding: $childInstanceData[childIndex]))
self.childIndex += 1
}
var body: some View {
ZStack {
ForEach(childInstances.indices, id: \.self) { index in
self.childInstances[index]
}
ForEach(childInstanceData.indices, id: \.self) { index in
Text("y: \(self.childInstanceData[index].height) : x: \(self.childInstanceData[index].width)")
.offset(y: CGFloat((index * 20) - 300))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct Child: View {
#Binding var stateBinding: CGSize
#State var isInitalDrag = true
#State var isOnce = true
#State var currentPosition: CGSize = .zero
#State var newPosition: CGSize = .zero
var body: some View {
Circle()
.frame(width: 50, height: 50)
.foregroundColor(.blue)
.offset(self.currentPosition)
.gesture(
DragGesture()
.onChanged { value in
if self.isInitalDrag && self.isOnce {
// Call function in ContentView here... How do I do it?
ContentView().addChild()
self.isOnce = false
}
self.currentPosition = CGSize(
width: CGFloat(value.translation.width + self.newPosition.width),
height: CGFloat(value.translation.height + self.newPosition.height)
)
self.stateBinding = self.currentPosition
}
.onEnded { value in
self.newPosition = self.currentPosition
self.isOnce = true
self.isInitalDrag = false
}
)
}
}
struct Child_Previews: PreviewProvider {
static var previews: some View {
Child(stateBinding: .constant(.zero))
}
}
Here is what I would do, I would create a function in the second view and invoke it under any circumstance, say when a button in the second view is pressed then invoke this function. And I would pass the function's callback in the main view.
Here is a code to demonstrate what I mean.
import SwiftUI
struct StackOverflow23: View {
var body: some View {
VStack {
Text("First View")
Divider()
// Note I am presenting my second view here and calling its function ".onAdd"
SecondView()
// Whenever this function is invoked inside `SecondView` then it will run the code in between these brackets.
.onAdd {
print("Run any function you want here")
}
}
}
}
struct StackOverflow23_Previews: PreviewProvider {
static var previews: some View {
StackOverflow23()
}
}
struct SecondView: View {
// Define a variable and give it default value
var onAdd = {}
var body: some View {
Button(action: {
// This button will invoke the callback stored in the variable, this can be invoked really from any other function. For example, onDrag call self.onAdd (its really up to you when to call this).
self.onAdd()
}) {
Text("Add from a second view")
}
}
// Create a function with the same name to keep things clean which returns a view (Read note 1 as why it returns view)
// It takes one argument which is a callback that will come from the main view and it will pass it down to the SecondView
func onAdd(_ callback: #escaping () -> ()) -> some View {
SecondView(onAdd: callback)
}
}
NOTE 1: The reason our onAdd function returns a view is because remember that swiftui is based on only views and every modifier returns a view itself. For example when you have a Text("test") and then add .foregroundColor(Color.white) to it, what essentially you are doing is that you are not modifying the color of the text but instead you are creating a new Text with a custom foregroundColor value.
And this is exactly what we are doing, we are creating a variable that can be initialized when calling SecondView but instead of calling it in the initializer we created it as a function modifier that will return a new instance of SecondView with a custom value to our variable.
I hope this makes sense. If you have any questions don't hesitate to ask.
And here is your code modified:
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var childInstances: [Child] = [Child(stateBinding: .constant(.zero))]
#State var childInstanceData: [CGSize] = [.zero]
#State var childIndex = 0
func addChild() {
self.childInstanceData.append(.zero)
self.childInstances.append(Child(stateBinding: $childInstanceData[childIndex]))
self.childIndex += 1
}
var body: some View {
ZStack {
ForEach(childInstances.indices, id: \.self) { index in
self.childInstances[index]
.onAddChild {
self.addChild()
}
}
ForEach(childInstanceData.indices, id: \.self) { index in
Text("y: \(self.childInstanceData[index].height) : x: \(self.childInstanceData[index].width)")
.offset(y: CGFloat((index * 20) - 300))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Child.swift
import SwiftUI
struct Child: View {
#Binding var stateBinding: CGSize
#State var isInitalDrag = true
#State var isOnce = true
#State var currentPosition: CGSize = .zero
#State var newPosition: CGSize = .zero
var onAddChild = {} // <- The variable to hold our callback
var body: some View {
Circle()
.frame(width: 50, height: 50)
.foregroundColor(.blue)
.offset(self.currentPosition)
.gesture(
DragGesture()
.onChanged { value in
if self.isInitalDrag && self.isOnce {
// Call function in ContentView here... How do I do it?
self.onAddChild() <- // Here is your solution
self.isOnce = false
}
self.currentPosition = CGSize(
width: CGFloat(value.translation.width + self.newPosition.width),
height: CGFloat(value.translation.height + self.newPosition.height)
)
self.stateBinding = self.currentPosition
}
.onEnded { value in
self.newPosition = self.currentPosition
self.isOnce = true
self.isInitalDrag = false
}
)
}
// Our function which will initialize our variable to store the callback
func onAddChild(_ callaback: #escaping () -> ()) -> some View {
Child(stateBinding: self.$stateBinding, isInitalDrag: self.isInitalDrag, isOnce: self.isOnce, currentPosition: self.currentPosition, newPosition: self.newPosition, onAddChild: callaback)
}
}
struct Child_Previews: PreviewProvider {
static var previews: some View {
Child(stateBinding: .constant(.zero))
}
}
Here's your answer within my code:
import SwiftUI
struct ContentView: View {
#State var childInstances: [Child] = []
#State var childInstanceData: [CGSize] = []
#State var childIndex = 0
func addChild() {
self.childInstanceData.append(.zero)
self.childInstances.append(Child(stateBinding: $childInstanceData[childIndex]))
self.childIndex += 1
}
var body: some View {
ZStack {
ForEach(childInstances.indices , id: \.self) { index in
self.childInstances[index]
.onAddChild {
print(self.childInstances.count)
self.addChild()
}
}
VStack {
ForEach(childInstanceData.indices, id: \.self) { index in
Text("\(index). y: \(self.childInstanceData[index].height) : x: \(self.childInstanceData[index].width)")
}
}
.offset(y: -250)
}
.onAppear {
self.addChild()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct Child: View {
#Binding var stateBinding: CGSize
var onAddChild = {} // <- The variable to hold our callback
#State var isInitalDrag = true
#State var isOnce = true
#State var currentPosition: CGSize = .zero
#State var newPosition: CGSize = .zero
var body: some View {
Circle()
.frame(width: 50, height: 50)
.foregroundColor(.blue)
.offset(self.currentPosition)
.gesture(
DragGesture()
.onChanged { value in
if self.isInitalDrag && self.isOnce {
// Call function in ContentView here:
self.onAddChild()
self.isOnce = false
}
self.currentPosition = CGSize(
width: CGFloat(value.translation.width + self.newPosition.width),
height: CGFloat(value.translation.height + self.newPosition.height)
)
self.stateBinding = self.currentPosition
}
.onEnded { value in
self.newPosition = self.currentPosition
self.isOnce = true
self.isInitalDrag = false
}
)
}
func onAddChild(_ callback: #escaping () -> ()) -> some View {
Child(stateBinding: self.$stateBinding, onAddChild: callback)
}
}
struct Child_Previews: PreviewProvider {
static var previews: some View {
Child(stateBinding: .constant(.zero))
}
}