Variable assignment with mutating functionality - swift

So I'm fairly new to SwiftUI & Swift (Javascript background) and I am going through the SwiftUI tutorial on Apple website.
When you get through the first section, you'll end up with LandmarkDetail.swift body ends up something like this:
var body: some View {
VStack {
MapView(coordinate: landmark.locationCoordinate)
.edgesIgnoringSafeArea(.top)
.frame(height: 300)
CircleImage(image: landmark.image)
.offset(x: 0, y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
Button(action: {
self.userData.landmarks[self.landmarkIndex].isFavorite.toggle()
}, label: {
if (self.userData.landmarks[self.landmarkIndex].isFavorite) {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
else {
Image(systemName: "star")
.foregroundColor(.gray)
}
})
}
HStack(alignment: .top) {
Text(landmark.park)
.font(.subheadline)
Spacer()
Text(landmark.state)
.font(.subheadline)
}
}
.padding()
Spacer()
}
.navigationBarTitle(Text(landmark.name), displayMode: .inline)
}
All good. So I decide to make some small incremental changes just to test the language. I noticed self.userData.landmarks[self.landmarkIndex] is used twice so I simply added that as a variable within the body and had the view return explicitly.
var userDataLandmark: Landmark = self.userData.landmarks[self.landmarkIndex]
return VStack {
No big deal. Then I replaced the repeating landmark:
Button(action: {
userDataLandmark.isFavorite.toggle()
}, label: {
if (userDataLandmark.isFavorite) {
Image(systemName: "star.fill")
Everything looked fine on the surface, but the toggle() functionality no longer works. Generally assuming this is some referencing/pointer stuff but If anyone could explain why that'd be great.

When you did:
var userDataLandmark: Landmark = self.userData.landmarks[self.landmarkIndex]
you created a copy of self.userData.landmarks[self.landmarkIndex], because Landmark is a value-type (a struct).
Now, when you do:
userDataLandmark.isFavorite.toggle()
you're toggling the copy. So, it doesn't change the .landmarks property of the observed environment variable userData, which is what would have caused the view's body to be recomputed.

Related

How to do a "reveal"-style collapse/expand animation in SwiftUI?

I'd like to implement an animation in SwiftUI that "reveals" the content of a view to enable expand/collapse functionality. The content of the view I want to collapse and expand is complex: It's not just a simple box, but it's a view hierarchy of dynamic height and content, including images and text.
I've experimented with different options, but it hasn't resulted in the desired effect. Usually what happens is that when I "expand", the whole view was shown right away with 0% opacity, then gradually faded in, with the buttons under the expanded view moving down at the same time. That's what happened when I was using a conditional if statement that actually added and removed the view. So that makes sense.
I then experimented with using a frame modifier: .frame(maxHeight: isExpanded ? .infinity : 0). But that resulted in the contents of the view being "squished" instead of revealed.
I made a paper prototype of what I want:
Any ideas on how to achieve this?
Something like this might work. You can modify the height of what you want to disclose to be 0 when hidden or nil when not so that it'll go for the height defined by the views. Make sure to clip the view afterwards so the contents are not visible outside of the frame's height when not disclosed.
struct ContentView: View {
#State private var isDisclosed = false
var body: some View {
VStack {
Button("Expand") {
withAnimation {
isDisclosed.toggle()
}
}
.buttonStyle(.plain)
VStack {
GroupBox {
Text("Hi")
}
GroupBox {
Text("More details here")
}
}
.frame(height: isDisclosed ? nil : 0, alignment: .top)
.clipped()
HStack {
Text("Cancel")
Spacer()
Text("Book")
}
}
.frame(maxWidth: .infinity)
.background(.thinMaterial)
.padding()
}
}
No, this wasn't trying to match your design, either. This was just to provide a sample way of creating the animation.
Consider the utilization of DisclosureGroup. The following code should be a good approach to your idea.
struct ContentView: View {
var body: some View {
List(0...20, id: \.self) { idx in
DisclosureGroup {
HStack {
Image(systemName: "person.circle.fill")
VStack(alignment: .leading) {
Text("ABC")
Text("Test Test")
}
}
HStack {
Image(systemName: "globe")
VStack(alignment: .leading) {
Text("ABC")
Text("X Y Z")
}
}
HStack {
Image(systemName: "water.waves")
VStack(alignment: .leading) {
Text("Bla Bla")
Text("123")
}
}
HStack{
Button("Cancel", role: .destructive) {}
Spacer()
Button("Book") {}
}
} label: {
HStack {
Spacer()
Text("Expand")
}
}
}
}
The result looks like:
I coded this in under 5 minutes. So of course the design can be optimized to your demands, but the core should be understandable.
import SwiftUI
struct TaskViewCollapsible: View {
#State private var isDisclosed = false
let header: String = "Review Page"
let url: String
let tasks: [String]
var body: some View {
VStack {
HStack {
VStack(spacing: 5) {
Text(header)
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.black)
.padding(.top, 10)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
Text(url)
.font(.system(size: 12, weight: .regular))
.foregroundColor(.black.opacity(0.4))
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
}
Spacer()
Image(systemName: self.isDisclosed ? "chevron.up" : "chevron.down")
.padding(.trailing)
.padding(.top, 10)
}
.onTapGesture {
withAnimation {
isDisclosed.toggle()
}
}
FetchTasks()
.padding(.horizontal, 20)
.padding(.bottom, 5)
.frame(height: isDisclosed ? nil : 0, alignment: .top)
.clipped()
}
.background(
RoundedRectangle(cornerRadius: 8)
.fill(.black.opacity(0.2))
)
.frame(maxWidth: .infinity)
.padding()
}
#ViewBuilder
func FetchTasks() -> some View {
ScrollView(.vertical, showsIndicators: true) {
VStack {
ForEach(0 ..< tasks.count, id: \.self) { value in
Text(tasks[value])
.font(.system(size: 16, weight: .regular))
.foregroundColor(.black)
.padding(.vertical, 0)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.frame(maxHeight: CGFloat(tasks.count) * 20)
}
}
struct TaskViewCollapsible_Previews: PreviewProvider {
static var previews: some View {
TaskViewCollapsible(url: "trello.com", tasks: ["Hello", "Hello", "Hello"])
}
}

SwiftUI View scrolls horizontal and vertical as well, instead of scrolling only vertically

I am building an app as a learning project. I ran into a problem, where one of my tabs scrolls vertically and horizontally for no reason.
There is no content outside the safe-area, and i can´t think of a reason for it to be scrolling sideways.
I already tried different things, replacing the GeometryReader with a VStack, specifying that i want it to be a vertical ScrollView. Adding .clipped to the end of the View as well as removing or replacing several other views
But nothing worked.
About the project:
The View is wrapped in a TabView and a NavigationView. All of the other tabs were wrapped the same way, and one also uses a ScrollView, yet does not have this problem.
The main Code:
struct InformationView: View {
#EnvironmentObject var model: ViewModel
var radialColor1 = Color.init(red: 225/255, green: 210/255, blue: 164/255)
var radialColor2 = Color.init(red: 233/255, green: 222/255, blue: 188/255)
var isShowingWarning = true
var body: some View {
GeometryReader { geo in
ScrollView(.vertical) {
VStack(alignment: .leading) {
Text("Here is a Text")
.padding(.vertical)
// W-fragen Stack
W_FragenCardView()
Text("Here is another Text")
.padding(.vertical)
Text("Kontakt:")
.font(.title3)
.bold()
ContactView()
}
.padding(.horizontal)
if isShowingWarning {
ZStack {
Rectangle()
.foregroundColor(Color.customRed())
Text("Here is a third Text")
.minimumScaleFactor(0.1)
.lineLimit(10)
.padding()
}
.frame(height: geo.size.height/5)
// .frame(height: 100)
}
Text("Current Theme: \(model.themes[1]!)")
}
}
.navigationBarTitle("Informationen", displayMode: .automatic)
}
}
This is were its wrapped:
struct ContentView: View {
#EnvironmentObject var model: ViewModel
var body: some View {
TabView {
NavigationView {
AnmeldungsView()
}
.tabItem {
Image(systemName: "square.and.pencil")
Text("Anmelden")
}
NavigationView {
FotosView()
}
.tabItem {
Image(systemName: "photo.fill.on.rectangle.fill")
Text("Fotos")
}
NavigationView {
InformationView()
}
.tabItem {
Image(systemName: "info.circle")
Text("Informationen")
}
}
}
}
And here an Image of how it looks when scrolling.
I hope this is sufficent to understand my problem. Any help or ideas are greatly appreciated.
P.S.: I´m new to stack overflow, so if a violated any rules please tell me :)
You should write an array([.horizontal, .vertical]) in ScrollView instead of .vertical.
change your code to this:
GeometryReader { geo in
ScrollView([.horizontal, .vertical]) {
VStack(alignment: .leading) {
Text("Here is a Text")
.padding(.vertical)
W_FragenCardView()
Text("Here is another Text")
.padding(.vertical)
Text("Kontakt:")
.font(.title3)
.bold()
ContactView()
}
.padding(.horizontal)
if isShowingWarning {
ZStack {
Rectangle()
.foregroundColor(Color.red)
Text("Here is a third Text")
.minimumScaleFactor(0.1)
.lineLimit(10)
.padding()
}
.frame(height: geo.size.height/5)
// .frame(height: 100)
}
}
And I hope it will works :)
It was a problem between geo.size.height/5 and the navigationBarTitle being on displayMode: .automatic.
This resulted in the available height changing and somehow bugged out. Either changing displayMode: to .inline or .large, or you replace the geo.size.height/5 w/ a hardcoded Int, i used 150.

SwiftUI Function declares an Opaque return type, inserting two buttons in one View?

I would like to build a simple SwiftUI ContentView.swift. In this App's view pane it would contain two buttons with their images implied. I thought about adding a function for each button one by one, then allowing SwiftUI to show each element. I have viewed some questions related to the return type, although it is confusing me as to where you would add a return type if I only have a Button(). My code is very short, is it easy to see where I went wrong where I did not include the return?
struct ContentView: View {
var body : some View {
func Button1(){
Button(action: {
print("Here is a bit of information.")
}) {
Image(systemName: "info")
.padding()
.background(Color.green)
.font(.largeTitle)
.foregroundColor(Color.orange)
.frame(width: 300, height: 600)
}
}
func Button2(){
Button(action: {
print("You have erased it.")
}) {
Image(systemName: "trash")
.padding()
.background(Color.red)
.font(.largeTitle)
.foregroundColor(Color.white)
.frame(width: 426, height: 620)
}
}
}
I am hoping that these two buttons will appear on the screen's first view, and I may then edit the action's they will both take after I understand the placement within the code. Thank you for your insight :)
Here's how you would do this:
struct ContentView: View {
var body: some View {
VStack {
Button(action: {
print("Here is a bit of information.")
}) {
Image(systemName: "info")
.padding()
.background(Color.green)
.font(.largeTitle)
.foregroundColor(Color.orange)
.frame(width: 300, height: 600)
}
Button(action: {
print("You have erased it.")
}) {
Image(systemName: "trash")
.padding()
.background(Color.red)
.font(.largeTitle)
.foregroundColor(Color.white)
.frame(width: 426, height: 620)
}
}
}
}

Modal picker not scrolling right SwiftUI

I created a modal but it seems to have a bug on the selection. When scrolling the left, it scrolls the right, I have to go to the very edge of the left to be able to scroll, this is how it looks:
import SwiftUI
struct ContentView: View {
#State var showingModal = false
#State var hours: Int = 0
#State var minutes: Int = 0
var body: some View {
ZStack {
VStack {
Button("Show me"){
self.showingModal = true
}
if $showingModal.wrappedValue {
VStack(alignment: .center) {
ZStack{
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.vertical)
// this one is it
VStack(spacing: 20) {
Text("Time between meals")
.bold().padding()
.frame(maxWidth: .infinity)
.background(Color.yellow)
.foregroundColor(Color.white)
HStack {
Spacer()
VStack {
Picker("", selection: $hours){
ForEach(0..<4, id: \.self) { i in
Text("\(i) hours").tag(i)
}
}
.frame(width: 150, height: 120)
.clipped()
}
VStack {
Picker("", selection: $minutes){
ForEach(0..<60, id: \.self) { i in
Text("\(i) min").tag(i)
}
}
.frame(width: 150, height: 120)
.clipped()
}
}
Spacer()
Button(action: {
self.showingModal = false
}){
Text("Close")
} .padding()
}
.frame(width:300, height: 300)
.background(Color.white)
.cornerRadius(20).shadow(radius: 20)
}
}
}
}
}
}
}
How can I fix that little bug? I tried playing around with the layout but no use... any help would be appreciated
What if I told you the reason your Picker not working was this line?
.cornerRadius(20).shadow(radius: 20)
Unfortunately, SwiftUI is still quite buggy and sometimes it doesn't do what it is supposed to do and especially Pickers are not that reliable. I guess we'll need to wait and see the next iteration of SwiftUI, but for now you can replace that line with the code below:
.mask(RoundedRectangle(cornerRadius: 20))
.shadow(radius: 20)
There are just modifiers which affect all view hierarchy (ie. all subviews) that can change resulting layout/presentation/behaviour. And .cornerRadius and .shadow are such type modifiers.
The solution is to apply (as intended) those modifiers only to entire constructed view, and here it is
.compositingGroup() // <<< fix !!
.cornerRadius(20).shadow(radius: 20)
where .compositionGroup is intended to make above view hierarchy flat rendered and all below modifiers applied to only to that flat view.

How can I fit a shape in swift ui to accommodate the length and width of a text view

ZStack {
RoundedRectangle(cornerRadius: 8)
.foregroundColor(.red)
.scaledToFit() //.frame(width: 200, height: 25)
HStack {
Image(systemName: "tag.fill")
.foregroundColor(.white)
Text("Tickets Not Available")
.font(.headline)
.foregroundColor(.white)
.fixedSize(horizontal: true, vertical: false)
}
}
.scaledToFit()
As you can see my views are placed in a zstack so that the rounded rectangle can be the background of the text view. I've tried so many different things like where to put the .scaledtofit and it just gives me wack results each time.
is this what you're after (note the Image.resizable):
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack{
RoundedRectangle(cornerRadius: 8).foregroundColor(.blue)
HStack{
Image(systemName: "tag.fill").resizable().padding(4).foregroundColor(.white).scaledToFit()
Text("Get Tickets").font(.headline).foregroundColor(.white)
}
}.fixedSize()
}
The question is a bit unclear but if you are trying to fit a shape inside the text view, and you are fine with getting rid of scaledToFit, then the code should be:
RoundedRectangle(cornerRadius: 8).foregroundColor(.red).frame(width: textView.width, height: textView.height)
Hope this helps, and hopefully you didn't need to use scaledToFit.
If you did tell me in comments.
A reusable ButtonStyle might be helpful here. Instead of a ZStack, using the .background modifier helps to keep the size of the Button contents:
struct RoundedButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
ZStack {
configuration.label
.font(.headline)
.padding()
.background(RoundedRectangle(cornerRadius: 8).foregroundColor(Color.blue))
}
}
}
Usage example:
Button(
action: {
print("Button Tapped")
},
label: {
HStack {
Image(systemName: "tag.fill")
Text("Tickets")
}
}
)
.buttonStyle(RoundedButtonStyle())