I am relatively new to Swiftui and am currently working on my second App which is much more complex that the first.
I have hit a wall on one particular requirement which I have been trying to resolve for a few days now and I am now going round in circles having used numerous videos and articles about State/Binding/ObservableObject etc.
I am developing a scoring App for Golf where the user will go through each hole in turn and enter their scores, various calculations will be done and displayed in the view. One of them is the total points scored. When they move onto the second hole the same process will be followed but there is one additional calculation which requires to add together the total points from Hole 1 and Hole 2. The various methods I have tried have either ended up in a value of 0 or more error messages that I care to remember and certainly too many to include in here!!
I would appreciate any help, I have included my code below (cut down for the purpose of this post) including some notes to explain what I need to do which I hope are clear. If anyone can adapt my code to something that works that would be great as I can then see how it all fits together.
Thanks in advance
START VIEW
// This screen is simply used to start a round. It will take you to Hole 1 and from there the navigationlink in Hole 1 will take you to Hole 2 and so on.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: Hole1View()) {
Text("Start Round")
.frame(width: 300, height: 30)
.font(.system(size: 18, weight: .bold, design: .rounded))
.lineLimit(1)
.multilineTextAlignment(.center)
.padding(18)
.background(Color.yellow)
.foregroundColor(Color.blue)
.cornerRadius(10)
.border(Color.blue, width: 10)
.cornerRadius(1)
}
}
}
}
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
HOLE 1 VIEW
// On this view you will enter various information and there will be various calculations to include in the View. One of the calculations is to calculate a value (Hole1totalpoints) which will be required in calculations in the next view.
import SwiftUI
struct Hole1View: View {
#State private var hole1gross = 0
var hole1totalpoints: Int {
let hole1points = hole1gross * 3
return hole1points
}
var body: some View {
// On this view you will enter various information and there will be various calculations to include in the View. One of the calculations is to calculate a value (Hole1totalpoints) which will be required in calculations in the next view.
Stepper("\(hole1gross)", value: $hole1gross, in: 0...15)
.frame(width: 300, height: 35, alignment: .leading)
.font(.system(size: 16, weight: .bold, design: .rounded))
.minimumScaleFactor(0.6)
.multilineTextAlignment(.leading)
.lineLimit(2)
Text("Calculated Value \(hole1totalpoints)")
NavigationLink(destination: Hole2View()) {
Text("Go to hole 2")
}
}
}
struct Hole1View_Previews: PreviewProvider {
static var previews: some View {
Hole1View()
}
}
HOLE 2 VIEW
// On this view you will enter various information and there will be various calculations to include in the View. One of the calculations is to calculate a value (Hole2totalpoints). One of the calculations needs to include the value hole1totalpoints from the previous view (Hole1View) to calculate the total points from the 2 views (hole1totalpoints+hole2totalpoints) which can be used in the View.
import SwiftUI
struct Hole2View: View {
#State private var hole2gross = 0
#State private var hole2totpoints = 0
var hole2totalpoints: Int {
let hole1points = hole2gross * 2
return hole1points
}
var body: some View {
Stepper("\(hole2gross)", value: $hole2gross, in: 0...15)
.frame(width: 300, height: 35, alignment: .leading)
.font(.system(size: 16, weight: .bold, design: .rounded))
.minimumScaleFactor(0.6)
.multilineTextAlignment(.leading)
.lineLimit(2)
Text("Calculated Value \(hole2totalpoints)")
}
}
struct Hole2View_Previews: PreviewProvider {
static var previews: some View {
Hole2View()
}
}
You can accomplish this with #ObservableObject or an #EnvironmentObject. I've chosen to show it here with passing an #ObservableObject via props, but you could easily change this to just injecting my ScoreManager object at the top with environmentObject as well:
struct ContentView: View {
#ObservedObject var scoreManager = ScoreManager()
var body: some View {
NavigationView {
NavigationLink(destination: Hole1View(scoreManager: scoreManager)) {
Text("Start Round")
.frame(width: 300, height: 30)
.font(.system(size: 18, weight: .bold, design: .rounded))
.lineLimit(1)
.multilineTextAlignment(.center)
.padding(18)
.background(Color.yellow)
.foregroundColor(Color.blue)
.cornerRadius(10)
.border(Color.blue, width: 10)
.cornerRadius(1)
}
}
}
}
class ScoreManager : ObservableObject {
#Published var hole1gross = 0
var hole1totalpoints: Int {
let hole1points = hole1gross * 3
return hole1points
}
#Published var hole2gross = 0
#Published var hole2totpoints = 0
var hole2totalpoints: Int {
//do any calculations here relating to hole 1 points, since you have access to both
let hole1points = hole2gross * 2
return hole1points
}
var hole1AndHole2Points : Int {
hole1totalpoints + hole2totalpoints
}
}
struct Hole1View: View {
#ObservedObject var scoreManager : ScoreManager
var body: some View {
Stepper("\(scoreManager.hole1gross)", value: $scoreManager.hole1gross, in: 0...15)
.frame(width: 300, height: 35, alignment: .leading)
.font(.system(size: 16, weight: .bold, design: .rounded))
.minimumScaleFactor(0.6)
.multilineTextAlignment(.leading)
.lineLimit(2)
Text("Calculated Value \(scoreManager.hole1totalpoints)")
NavigationLink(destination: Hole2View(scoreManager: scoreManager)) {
Text("Go to hole 2")
}
}
}
struct Hole2View: View {
#ObservedObject var scoreManager : ScoreManager
var body: some View {
Stepper("\(scoreManager.hole2gross)", value: $scoreManager.hole2gross, in: 0...15)
.frame(width: 300, height: 35, alignment: .leading)
.font(.system(size: 16, weight: .bold, design: .rounded))
.minimumScaleFactor(0.6)
.multilineTextAlignment(.leading)
.lineLimit(2)
Text("Calculated Value \(scoreManager.hole2totalpoints)")
Text("Both holes: \(scoreManager.hole1AndHole2Points)")
}
}
ContentView now creates a ScoreManager. It gets passed down through the other views in the NavigationLinks. The other views end up with the same version of that object (it isn't getting recreated in the new views -- they all have the same reference to the original). Then, in the original, you have access to all the scores you want. I created hole1AndHole2Points which I think does what you're describing. You can see the value reflected in the last Text item on Hole 2.
If you wanted to go the environment object route, the code will be very similar. More reading on that system: https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views
Related
I apply ".animation (.easeIn)" but it is deprecated.
tells me: animation 'has been deprecated in iOS 15.0: use instead with Animation or animation (: value :), but animation (: value :) I don't know which value I need to pass. I can't create animations that start slowly. Can you please help me understand how I can properly use animation (_: value :) to make animation work? I have to create the animation that when I move the slider, the images slowly recompose in space according to how much they are enlarged or reduced.
thank you.
'''
import SwiftUI
struct GalleryView: View {
// MARK: - PROPERTIES
#State private var selectedAnimal: String = "lion"
let animals: [Animal] = Bundle.main.decode("animals.json")
let haptics = UIImpactFeedbackGenerator(style: .medium)
#State private var gridLayout: [GridItem] = [GridItem(.flexible())]
#State private var gridColumn: Double = 3.0
func gridSwitch() {
gridLayout = Array(repeating: .init(.flexible()), count: Int(gridColumn))
}
// MARK: - BODY
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .center, spacing: 30) {
// MARK: - IMAGE
Image(selectedAnimal)
.resizable()
.scaledToFit()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 8))
// MARK: - SLIDER
Slider(value: $gridColumn, in: 2...4, step: 1) // se aumento il valore nello slider ci saranno piĆ¹ sezioni
.padding(.horizontal)
.onChange(of: gridColumn, perform: { value in
gridSwitch()
})
// MARK: - GRID
LazyVGrid(columns: gridLayout, alignment: .center, spacing: 10) {
ForEach(animals) { item in
Image(item.image)
.resizable()
.scaledToFit()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 1))
.onTapGesture {
selectedAnimal = item.image
haptics.impactOccurred()
}
} //: LOOP
} //: GRID
.animation(.easeIn)
.onAppear(perform: {
gridSwitch()
})
} //: VSTACK
.padding(.horizontal, 10)
.padding(.vertical,50)
} //: SCROLL
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(MotionAnimationView())
}
}
// MARK: - PREVIEW
struct GalleryView_Previews: PreviewProvider {
static var previews: some View {
GalleryView()
}
}
As the error says, .animation() is deprecated, with iOS 15, you need to use .animation(value:) now. where the value is the binding that you want your animation to be triggered, in your case, I assume it is selectedAnimal.
Applying this would be something like
VStack {
}
.animation(.easeIn, value: selectedAnimal)
As discussed in comments, if you want your gridLayout to be animatable, it is a little bit more tricky. Because Arrays has to be Equatable if you want them to be animatable, since extending the GridItem is not a good solution, I came up with this:
delete your .animation method change your gridSwitch function with this:
struct GalleryView: View {
// ... irrelevant code
#State private var isGridChanged = false
func gridSwitch() {
if (isGridChanged) {
withAnimation {
gridLayout = Array(repeating: .init(.flexible()), count: Int(gridColumn))
}
}
else {
gridLayout = Array(repeating: .init(.flexible()), count: Int(gridColumn))
isGridChanged = true
}
}
isGridChanged is required because as you're changing your gridLayout when your View is initialized, it causes a weird bug that everything is getting scaled down when app launches because of withAnimation.
So in the past I have been tracking the scroll position using a scroll view but I've fallen into a situation where I need to track the position using a List. I am using a List because I want some of the built in real estate to create my views such as the default List styles.
I can get the value using PreferenceKeys, but the issue is when I scroll to far upwards, the PreferenceKey value will default back to its position 0, breaking my show shy header view logic.
This is the TrackableListView code
struct TrackableListView<Content: View>: View {
let offsetChanged: (CGPoint) -> Void
let content: Content
init(offsetChanged: #escaping (CGPoint) -> Void = { _ in }, #ViewBuilder content: () -> Content) {
self.offsetChanged = offsetChanged
self.content = content()
}
var body: some View {
List {
GeometryReader { geometry in
Color.clear.preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("ListView")).origin)
}
.frame(width: 0, height: 0)
content
.offset(y: -10)
}
.coordinateSpace(name: "ListView")
.onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: offsetChanged)
}
}
private struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { }
}
And this is my ContentView:
struct ContentView: View {
#State private var contentOffset = CGFloat(0)
#State private var offsetPositionValue: CGFloat = 0
#State private var isShyHeaderVisible = false
var body: some View {
NavigationView {
ZStack {
TrackableListView { offset in
withAnimation {
contentOffset = offset.y
}
} content: {
Text("\(contentOffset)")
}
.overlay(
ZStack {
HStack {
Text("Total points")
.foregroundColor(.white)
.lineLimit(1)
Spacer()
Text("20,000 pts")
.foregroundColor(.white)
.padding(.leading, 50)
}
.padding(.horizontal)
.padding(.vertical, 8)
.frame(width: UIScreen.main.bounds.width)
.background(Color.green)
.offset(y: contentOffset < 50 ? 0 : -5)
.opacity(contentOffset < 50 ? 1 : 0)
.transition(.move(edge: .top))
}
.frame(maxHeight: .infinity, alignment: .top)
)
}
.navigationTitle("Hello")
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarTitleDisplayMode(.inline)
.frame(maxHeight: .infinity, alignment: .top)
.background(AccountBackground())
}
}
}
The issue was that Other Views in my hierarchy (probably introduced by SwiftUI) could be sending the default value, which is why you start getting zero sometimes.
What I needed was a way to determine when to use or forward the new value, versus when to ignore it.
To do this I had to make my value optional GCPoint, with a nil default value, then in your reduce method when using PreferenceKeys you have to do:
if let nextValue = nextValue() {
value = nextValue
}
Then make sure your CGPoint is an optional value.
I'm beginning to try out SwiftUI. I want the Circle is randomly changing the color from red or green.
struct ContentView: View {
#State var colorCircle = [".red", ".green"]
var body: some View {
ZStack {
Circle()
.fill(Color.colorCircle.randomElement())
.frame(width: 100,
height: 100)
.position(x: 250,
y: 320)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
First make your colorCircle array to contain Colors and not Strings.
#State var colorCircle: [Color] = [.red, .green]
Also note you're trying to access colorCircle from inside the Color struct:
Color.colorCircle.randomElement()
As you declared colorCircle in the ContentView you can use it like:
self.colorCircle.randomElement()
or (shorter):
colorCircle.randomElement()
Then you can use randomElement:
struct ContentView: View {
#State var colorCircle: [Color] = [.red, .green]
var body: some View {
ZStack {
Circle()
.fill(colorCircle.randomElement()!)
.frame(width: 100,
height: 100)
.position(x: 250,
y: 320)
}
}
}
Note that randomElement() returns an optional so you can force-unwrap it (!) if you're sure your array is not empty.
However, as a good practice, I recommend not to use ! and provide default values if possible:
.fill(colorCircle.randomElement() ?? .red)
I've got some simple code that creates an array (newFactoid) by calling a function (createSampleData) which it then holds as a State. A view displays record 1 of the array. So far so good. However, I'm trying to insert a button into the view which calls a simple function (refreshFactoid) which shuffles the array, in theory causing the view to refresh. The problem is I'm getting the above error when I insert the button/function. If I remove the button the error disappears. Any pointers/help appreciated.
import SwiftUI
struct ContentView : View {
#State private var newFactoid = createSampleData()
var body: some View {
VStack {
// Display Category
Text(newFactoid[1].category).fontWeight(.thin)
.font(.title)
// Display Image
Image("Factoid Image \(newFactoid[1].ref)")
.resizable()
.scaledToFit()
.cornerRadius(15)
.shadow(color: .gray, radius: 5, x:5, y:5)
.padding(25)
// Display Factoid
Text("A: \(newFactoid[1].fact)")
.padding(25)
.multilineTextAlignment(.center)
.background(Color.white)
.cornerRadius(15)
.shadow(color: .gray, radius: 5, x:5, y:5)
.padding(25)
// Display Odds
Text("B: \(newFactoid[1].odds)").fontWeight(.ultraLight)
.font(.title)
.padding()
.frame(width: 150, height: 150)
.clipShape(Circle())
.multilineTextAlignment(.center)
.overlay(Circle().stroke(Color.white, lineWidth: 2))
.shadow(color: .gray, radius: 5, x: 5, y: 5)
// Refresh Button
Button (action: {refreshFactoid()}) {
Text("Press To Refresh Data")
}
// Refresh Function
func refreshFactoid() {
newFactoid.shuffle()
}
} // End of VStack Closure
}
}
struct TextUIView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The func cannot be declared in the VStack.
Either trigger the shuffle in the action block of the Button:
Button(action: { self.newFactoid.shuffle() }) {
Text("Press To Refresh Data")
}
or declare the function in the View struct:
struct ContentView: View {
#State private var newFactoid = createSampleData()
var body: some View {
VStack {
// ...
// Refresh Button
Button(action: { self.refreshFactoid() }) {
Text("Press To Refresh Data")
}
} // End of VStack Closure
}
// Refresh Function
func refreshFactoid() {
newFactoid.shuffle()
}
}
just remove
// Refresh Function
func refreshFactoid() {
newFactoid.shuffle()
}
} // End of VStack Closure
from the body
I have a horizontal scroll view where I'd like to set a position programmatically. Here is the body of the view:
let radius = CGFloat(25)
let scrollWidth = CGFloat(700)
let scrollHeight = CGFloat(100)
let spacing = CGFloat(20)
let circleDiameter = CGFloat(50)
...
var body: some View {
GeometryReader { viewGeometry in
ScrollView () {
VStack(spacing: 0) {
Spacer(minLength: spacing)
Circle()
.fill(Color.black.opacity(0.5))
.scaledToFit()
.frame(width: circleDiameter, height: circleDiameter)
ScrollView(.horizontal) {
Text("The quick brown fox jumps over the lazy dog. Finalmente.")
.font(Font.title)
.frame(width: scrollWidth, height: scrollHeight)
.foregroundColor(Color.white)
.background(Color.white.opacity(0.25))
}
.frame(width: viewGeometry.size.width, height: scrollHeight)
.padding([.top, .bottom], spacing)
Circle()
.fill(Color.white.opacity(0.5))
.scaledToFit()
.frame(width: circleDiameter, height: circleDiameter)
Spacer(minLength: spacing)
}
.frame(width: viewGeometry.size.width)
}
.background(Color.orange)
}
.frame(width: 324 / 2, height: spacing * 4 + circleDiameter * 2 + scrollHeight) // testing
.cornerRadius(radius)
.background(Color.black)
}
How do I change this code so that I can get the current position of "The quick brown fox" and restore it at a later time? I'm just trying to do something like we've always done with contentOffset in UIKit.
I can see how a GeometryReader might be useful to get the content's current frame, but there's no equivalent writer. Setting a .position() or .offset() for the scroll view or text hasn't gotten me anywhere either.
Any help would be most appreciated!
I've been playing around with a solution and posted a Gist to what I have working in terms of programmatically setting content offsets https://gist.github.com/jfuellert/67e91df63394d7c9b713419ed8e2beb7
With the regular SwiftUI ScrollView, as far as I can tell, you can get the position with GeometryReader with proxy.frame(in: .global).minY (see your modified example below), but you cannot set the "contentOffset".
Actually if you look at the Debug View Hierarchy you will notice that our content view is embedded in an internal SwiftUI other content view to the scrollview. So you will offset vs this internal view and not the scroll one.
After searching for quite a while, I could not find any way to do it with the SwiftUI ScrollView (I guess will have to wait for Apple on this one). The best I could do (with hacks) is a scrolltobottom.
UPDATE: I previously made a mistake, as it was on the vertical scroll. Now corrected.
class SGScrollViewModel: ObservableObject{
var scrollOffset:CGFloat = 0{
didSet{
print("scrollOffset: \(scrollOffset)")
}
}
}
struct ContentView: View {
public var scrollModel:SGScrollViewModel = SGScrollViewModel()
let radius = CGFloat(25)
let scrollWidth = CGFloat(700)
let scrollHeight = CGFloat(100)
let spacing = CGFloat(20)
let circleDiameter = CGFloat(50)
var body: some View {
var topMarker:CGFloat = 0
let scrollTopMarkerView = GeometryReader { proxy -> Color in
topMarker = proxy.frame(in: .global).minX
return Color.clear
}
let scrollOffsetMarkerView = GeometryReader { proxy -> Color in
self.scrollModel.scrollOffset = proxy.frame(in: .global).minX - topMarker
return Color.clear
}
return GeometryReader { viewGeometry in
ScrollView () {
VStack(spacing: 0) {
Spacer(minLength: self.spacing)
Circle()
.fill(Color.black.opacity(0.5))
.scaledToFit()
.frame(width: self.circleDiameter, height: self.circleDiameter)
scrollTopMarkerView.frame(height:0)
ScrollView(.horizontal) {
Text("The quick brown fox jumps over the lazy dog. Finally.")
.font(Font.title)
.frame(width: self.scrollWidth, height: self.scrollHeight)
.foregroundColor(Color.white)
.background(Color.white.opacity(0.25))
.background(scrollOffsetMarkerView)
}
.frame(width: viewGeometry.size.width, height: self.scrollHeight)
.padding([.top, .bottom], self.spacing)
Circle()
.fill(Color.white.opacity(0.5))
.scaledToFit()
.frame(width: self.circleDiameter, height: self.circleDiameter)
Spacer(minLength: self.spacing)
}
.frame(width: viewGeometry.size.width)
}
.background(Color.orange)
}
.frame(width: 324 / 2, height: spacing * 4 + circleDiameter * 2 + scrollHeight) // testing
.cornerRadius(radius)
.background(Color.black)
}
}
I messed around with several solutions involving a ScrollView with one or more GeometryReaders, but ultimately I found everything easier if I just ignored ScrollView and rolled my own using View.offset(x:y:) and a DragGesture:
This allows the LinearGradient to be panned by either dragging it like a ScrollView, or by updating the binding, in the case via a Slider
struct SliderBinding: View {
#State var position = CGFloat(0.0)
#State var dragBegin: CGFloat?
var body: some View {
VStack {
Text("\(position)")
Slider(value: $position, in: 0...400)
ZStack {
LinearGradient(gradient: Gradient(colors: [.blue, .red]) , startPoint: .leading, endPoint: .trailing)
.frame(width: 800, height: 200)
.offset(x: position - 400 / 2)
}.frame(width:400)
.gesture(DragGesture()
.onChanged { gesture in
if (dragBegin == nil) {
dragBegin = self.position
} else {
position = (dragBegin ?? 0) + gesture.translation.width
}
}
.onEnded { _ in
dragBegin = nil
}
)
}
.frame(width: 400)
}
}
Clamping the drag operation to the size of the scrolled area is omitted for brevity. This code allows horizontal scrolling, use CGPoints instead of CGSize to implement it horizontally and vertically.