How do you call a function from a subview in SwiftUI? - 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))
}
}

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

Why Timer Change Will Trigger LazyVGrid View Update?

import SwiftUI
struct ContentView: View {
#State private var set = Set<Int>()
#State private var count = "10"
private let columns:[GridItem] = Array(repeating: .init(.flexible()), count: 3)
#State private var timer:Timer? = nil
#State private var time = 0
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(Array(set)) { num in
Text(String(num))
}
}
}
.frame(width: 400, height: 400, alignment: .center)
HStack{
TextField("Create \(count) items", text: $count)
Button {
createSet(count: Int(count)!)
} label: {
Text("Create")
}
}
if let _ = timer {
Text(String(time))
.font(.title2)
.foregroundColor(.green)
}
HStack {
Button {
time = 100
let timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
time -= 10
if time == 0 {
self.timer?.invalidate()
self.timer = nil
}
}
self.timer = timer
} label: {
Text("Start Timer")
}
Button {
self.timer?.invalidate()
self.timer = nil
} label: {
Text("Stop Timer")
}
}
}
.padding()
}
private func createSet(count:Int) {
set.removeAll(keepingCapacity: true)
repeat {
let num = Int.random(in: 1...10000)
set.insert(num)
} while set.count < count
}
}
extension Int:Identifiable {
public var id:Self { self }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I made a break point on Text(String(num)). Every time the timer was trigger, the GridView updated. Why this happened? As the model of grid didn't change.
Updated
If I put the timer in another view, the grid view wouldn't be trigger.
import SwiftUI
struct ContentView: View {
#State private var set = Set<Int>()
#State private var count = "10"
private let columns:[GridItem] = Array(repeating: .init(.flexible()), count: 3)
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(Array(set)) { num in
Text(String(num))
}
}
}
.frame(width: 400, height: 400, alignment: .center)
HStack{
TextField("Create \(count) items", text: $count)
Button {
createSet(count: Int(count)!)
} label: {
Text("Create")
}
}
TimerView()
}
.padding()
}
private func createSet(count:Int) {
set.removeAll(keepingCapacity: true)
repeat {
let num = Int.random(in: 1...10000)
set.insert(num)
} while set.count < count
}
}
extension Int:Identifiable {
public var id:Self { self }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct TimerView: View {
#State private var timer:Timer? = nil
#State private var time = 0
var body: some View {
if let _ = timer {
Text(String(time))
.font(.title2)
.foregroundColor(.green)
}
HStack {
Button {
time = 100
let timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
time -= 10
if time == 0 {
self.timer?.invalidate()
self.timer = nil
}
}
self.timer = timer
} label: {
Text("Start Timer")
}
Button {
self.timer?.invalidate()
self.timer = nil
} label: {
Text("Stop Timer")
}
}
}
}
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
TimerView()
}
}
That´s pretty much how SwiftUI works. Every change to a #State var triggers the View to reevaluate. If you put your ForEach in another view it will only reevaluate if you change a var that changes that view. E.g. set or columns.
struct ExtractedView: View {
var columns: [GridItem]
var set: Set<Int>
var body: some View {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(Array(set)) { num in
Text(String(num))
}
}
}
.frame(width: 400, height: 400, alignment: .center)
}
}
It is encouraged in SwiftUI to make many small Views. The system driving this is pretty good in identifying what needs to be changed and what not. There is a very good WWDC video describing this.
WWDC

SwiftUI: is possible Gesture var move into EnvironmentObject?

I am not familiar with SWIFTUI and closure. Here move some gesture code to extension function, except var drag.
For Gesture, Is is possible to move the drag into environmentObject? How to initialize ?
class ViewState:ObservableObject {
#Published var isDragging = false
#Published var size: CGFloat = 100.0
}
struct ContentView: View {
#StateObject var viewState:ViewState=ViewState()
var drag:some Gesture { isDrag(viewState) }
var body: some View {
Circle()
.fill(viewState.isDragging ? Color.red: Color.blue)
.frame(width:viewState.size, height:viewState.size, alignment: .center)
.gesture(drag)
}
}
extension ContentView {
func isDrag(_ viewState:ViewState)->some Gesture {
DragGesture()
.onChanged {_ in
viewState.isDragging = true
viewState.size += 10.0
}
.onEnded{_ in
viewState.isDragging = false
viewState.size = 100.0
}
}
}
Done, Closure is function! Now, I can move this code to other views, but still update with view risk. and declared data (environmentObject) is not independent.
class ViewState:ObservableObject {
#Published var isDragging = false
#Published var size: CGFloat = 100.0
}
struct ContentView: View {
#StateObject var viewState:ViewState=ViewState()
// var drag:some Gesture { }
var body: some View {
Circle()
.fill(viewState.isDragging ? Color.red: Color.blue)
.frame(width:viewState.size, height:viewState.size, alignment: .center)
.isDragged(viewState)
}
}
extension View {
func drag(_ viewState:ViewState)->some Gesture {
DragGesture()
.onChanged {_ in
viewState.isDragging = true
viewState.size += 10.0
}
.onEnded{_ in
viewState.isDragging = false
viewState.size = 100.0
}
}
#ViewBuilder func isDragged(_ viewState:ViewState) -> some View {
self.gesture(drag(viewState))
}
}

How to observe updates in binded values SwiftUI

I'm not sure if I created my custom TextField properly, because I am unable to observe the value changes to an #Binded text. Running the following code, you may observe that print(text) is not executed when you manually enter text into the text field.
import SwiftUI
#main
struct TestOutWeirdTextFieldApp: App {
#State var text: String = "" {
didSet {
print(text)
}
}
var body: some Scene {
WindowGroup {
StandardTextField(placeholderText: "Enter text", defaultText: $text)
}
}
}
struct StandardTextField: View {
#State var placeholderText: String {
didSet {
print(#line)
print(placeholderText)
}
}
#Binding var defaultText: String {
didSet {
print(#line)
print(defaultText)
}
}
#State var systemImage: String?
#State var underlineColor: Color = .accentColor
#State var edges: Edge.Set = .all
#State var length: CGFloat? = nil
#State var secure: Bool = false
var body: some View {
HStack {
if secure {
SecureField(placeholderText, text: $defaultText)
.foregroundColor(underlineColor)
} else {
TextField(placeholderText, text: $defaultText)
.foregroundColor(underlineColor)
}
if let systemImage = systemImage {
Image(systemName: systemImage)
.foregroundColor(.white)
}
}
.overlay(
Rectangle()
.frame(height: 2)
.padding(.top, 35)
)
.foregroundColor(underlineColor)
.padding(edges, length)
}
}
struct StandardTextView_Previews: PreviewProvider {
static var previews: some View {
StandardTextField(placeholderText: "Placement text", defaultText: .constant("")).previewLayout(.sizeThatFits)
}
}
Instead of didSet you need to use .onChange(of: modifier, like
HStack {
// ... your code here
}
.onChange(of: defaultText) { print($0) } // << this !!
.overlay(

SwiftUI different actions on the same Custom Alert actions

I created a custom alert. I want the product to be added to the basket when the Ok button on Alert is clicked on the first screen. When the Ok button is pressed on the second screen, the purchase of the product is requested. I called the same alert on 2 pages and I want it to take different actions. I couldn't do that with #Escaping.
AlertView
struct AlertView: View {
#Binding var openShowAlert: Bool
#State var closeShowAlert: Bool = false
#State var openState: CGFloat = -UIScreen.main.bounds.height
#State var closeState: CGFloat = UIScreen.main.bounds.height
var title: String = ""
var message: String = ""
var okButtonText: String = ""
var cancelButtonText: String = ""
var body: some View {
VStack {
Text(title)
.michromaFont(size: 20)
.padding(.top)
Spacer()
Text(message)
.michromaFont(size: 18)
Spacer()
HStack {
Button(action: {
self.openShowAlert = false
openState = -UIScreen.main.bounds.height
closeState = UIScreen.main.bounds.height
}) {
Text(cancelButtonText)
.foregroundColor(.red)
}
Spacer()
Button(action: {}) {
Text(okButtonText)
}
}
.michromaFont(size: 18)
.padding([.horizontal, .bottom])
}
.neumorphisimBackground(width: 300, height: 200)
.offset(y: self.openShowAlert ? self.openState : self.closeState)
.animation(.easeInOut)
.onChange(of: self.openShowAlert, perform: { value in
if value {
self.openState = .zero
}
})
}
}
DetailView
On this screen, click Alert presentation to add the product to the cart.
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode
var device = UIDevice.current.userInterfaceIdiom
#State var width: CGFloat = 300
#State var height: CGFloat = 450
#Binding var text: String
#State var showAlert: Bool = false
var body: some View {
ZStack() {
......
AlertView(openShowAlert: self.$showAlert)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
CartView Click I am providing an alert on this screen to purchase the product.
struct CartView: View {
#State var cartList = [1,2,3,4,5,6,7,8,9]
#Environment(\.presentationMode) var presentationMode
#State var showAlert: Bool = false
var body: some View {
ZStack(alignment: .top) {
.....
AlertView(openShowAlert: self.$showAlert)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
How can I send two different actions in the same alert.
Hmm, I don't see why it shouldn't work with a closure. Have you tried passing over a closure like so?
struct AlertView: View {
...
var okButtonAction: () -> ()
var body: some View {
...
Button(action: okButtonAction) {
Text(okButtonText)
}
}
}
Usage
AlertView(openShowAlert: self.$showAlert) {
// Your custom code
}
Alternative Idea
You could work with Combine and create a publisher with a specific key to identify the sender screen. Then you can put your custom code inside .onReceive().