The code below hide a text view behind a navigationBar based on Network status and it works great. The only problem is that we see the text going to hide behind the navigation bar at the first loading of the page.
How can i fix that? I want the text view to be already hidden at the start (Y Position -20)...
import SwiftUI
struct TestZStackNavigationView: View {
let screenSize: CGRect = UIScreen.main.bounds
#ObservedObject var online = NetStatus()
var body: some View {
NavigationView {
Text("NoNetworkTitle")
.fontWeight(.bold)
.foregroundColor(Color.white)
.frame(width: screenSize.width, height: 40, alignment: .center)
.background(Color.red)
.position(x: screenSize.width / 2, y: self.online.connected ? -20 : 20)
.animation(.easeIn(duration: 0.5), value: self.online.connected)
.navigationBarTitle(Text("Navigation Bar Title"), displayMode:.inline)
}
}
}
Ok i got it!
By default 'connected' is set to false in NetStatus() class.
I set it to true and now it's working like i want. ;-)
Related
I am writing a swiftUI app for macOS.
Consider this situation: I have a scroll view containing an image that is resizable. If I resize the window to smaller than the image, the scroll views bars appear as expected. If I use .scaleEffect(someStateVar) to resize the image (by changing someStateVar), the scroll view content does not update to the new size of the image.
Other than wrapping NSScrollView into a swiftUI view, how do I indicate to the swiftUI scrollview that the content size has changed?
Example that shows scaling the image does not set the "content size" of the scrollview. When you size the clock to large than the scroll view, the scroll bars do not appear and you cannot scroll. When you try to scroll, you briefly see the hidden areas but the view jumps back to the original location.
import SwiftUI
struct MyImageView: View {
#State var scale: CGFloat = 1
var theImage: some View{
ScrollView([.horizontal, .vertical]){
VStack{
HStack{
Image(systemName: "clock.fill")
.scaleEffect(scale)
}
}
}
}
var body: some View {
theImage
Slider(value: $scale, in: 1...1000)
}
}
struct ContentView: View {
var body: some View {
MyImageView()
}
}
You can use the .frame modifier to explicitly set the size of the VStack. The downside is that you have to know the full dimensions.
var theImage: some View{
ScrollView([.horizontal, .vertical]){
VStack{
HStack{
Image(systemName: "clock.fill")
.scaleEffect(scale)
}
}
.frame(width: 50 * scale, height: 50 * scale, alignment: .center)
}
}
I have found that this trim animation here animate path stroke drawing in SwiftUI works as expected if the MyLines view appears in the ContentView (root view) of the app or when it appears as destination of a navigation link. However, SwiftUI seems to ignore the animation duration if the view appears inside of a custom view - I created an overlay view that transitions in from the right.
I only took that line trimming animation as an example - this bug (if it is one) also seems to occur with other animations, e.g. some view changing its height on appear.
I tried changing the duration. if I double it (e.g. from 2 seconds to 4), the actual animation duration does not seem to change...
struct ContentView: View {
#State var showOverlay: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink("My Lines (Nav Link)", destination: MyLines(height: 200, width: 250))
Button(action: {
self.showOverlay.toggle()
}, label: {
Text("My Lines (overlay)")
})
}
}.overlayView(content: {
VStack {
HStack{
Button(action: { self.showOverlay = false}, label: {
Text("Back")
})
Spacer()
}.padding(.top, 40).padding(.horizontal, 15)
MyLines(height: 200, width: 250)
Spacer()
}
}, background: {
Color(.systemBackground)
}, show: $showOverlay, size: nil, transition: AnyTransition.move(edge: .trailing).animation(.easeInOut(duration: 0.3)))
}
}
Here is the code of MyLine again - I deliberately delay the animation by 1 second so that it becomes clear that the problem is not caused by a too long transition of the overlay view in which the Line view exists.
import SwiftUI
struct MyLines: View {
var height: CGFloat
var width: CGFloat
#State private var percentage: CGFloat = .zero
var body: some View {
Path { path in
path.move(to: CGPoint(x: 0, y: height/2))
path.addLine(to: CGPoint(x: width/2, y: height))
path.addLine(to: CGPoint(x: width, y: 0))
}
.trim(from: 0, to: percentage) // << breaks path by parts, animatable
.stroke(Color.black, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
withAnimation(.easeOut(duration: 2.0)) {
self.percentage = 1.0
}
})
}.padding()
}
}
I have set up a complete project here that should illustrate the problem and also includes the overlay view:
https://github.com/DominikButz/TrimAnimationBug
By the way, this duration problem does not disappear after removing NavigationView and only use the overlay view.
You might wonder, why use the custom overlay instead of the boiler plate navigation? I found that NavigationView causes a lot of problems:
difficult to change the background colour
does not support custom transitions, e.g. with a matched geometry effect (the overlay view I created can appear with any type of transition)
causes weird crashes (e.g. related to matchedGeometryEffect)
prevents the .hideStatusBar modifier to work properly
etc. etc.
Sorry, just found the culprit. I had left an animation(.spring()) modifier on the Overlay View. that seems to confuse SwiftUI somehow. after removing the modifier, it works as expected. Seems it helps writing down the problem to see the solution more easily...
I'm developing MacOS App with SwiftUI.
Let's say we have a fixed size window, which will show a scrollable image.
Now I want to create a feature which can let user to scale the image, and can scroll and view the scaled image inside the window.
The problem is, once I scaled up the image, the scroll area seems not expended together, and the scroll area is not big enough to reach each corner of the image.
struct ContentView: View {
var body: some View {
ScrollView([.horizontal, .vertical] , showsIndicators: true ){
Image("test")
.scaleEffect(2)
}
.frame(width: 500, height: 500)
}
}
I have tried to set the Image's frame by using GeometryReader, but got the same result.
MacOS 11.1, Xcode 12.3
Thanks!
.scaleEffect seems to perform a visual transform on the view without actually affecting its frame, so the ScrollView doesn't know to accommodate the bigger size.
.resizable() and .frame on the image seem to do the trick:
struct ContentView: View {
#State private var scale : CGFloat = 1.0
var body: some View {
VStack {
ScrollView([.horizontal, .vertical] , showsIndicators: true ){
Image(systemName: "plus.circle")
.resizable()
.frame(width: 300 * scale,height: 300 * scale)
}
.frame(width: 500, height: 500)
Slider(value: $scale, in: (0...5))
}
}
}
I am trying to increase the touchable area of a button inside a NavigationView. It does not work even though the area is made bigger. My code is below:
var body: some View {
NavigationView {
List(taskStore.tasks) { tasks in
Text(tasks.name)
}
.navigationBarTitle("Tasks")
.navigationBarItems(
trailing: Button(action: {
self.modalIsPresented = true
}){
Image(systemName: "plus")
.frame(width: 200, height: 200)
.contentShape(Rectangle())
.background(Color.yellow)
})}
The green area is touchable and the red area isn't touchable.
I found a solution online that works. However this solution only works for a button that is NOT in the NavigationView. So if I put the button in "some view" like the following below, it works as per the solution:
var body: some View {
Button(action: {self.modalIsPresented = true} ) {
Text("Default padding")
.padding(50)
.background(Color.yellow)
}}}
But when I put the button in a Navigation View like my code, the yellow area is not touchable. How can I get the whole yellow area (red box) to be touchable like the solution?
Thanks :D
Example of solution:
If you want a button in the navigation bar, it is only going to be clickable inside the navigation bar, no matter what you try to set the image's frame at, and the NavigationView determines that height, no matter what the children- the button, in this case- may want.
Historically, changing the height of the NavigationBar has not been supported: see the comments here
Now you could do something funky with ZStacks- put a button on top of the navigation view, perhaps- but you're not going to be able to put anything larger than the set height inside the navigation bar.
I think you may find your desired effect inside the .navigationBarItems(trailing:) if you use .contentShape(Rectangle()) modifier. Or you could use other shapes to suit your needs. Then adjust the size of the .frame to adjust the tappable area as desired. Here is a quick code example.
struct ContentView: View {
var body: some View {
NavigationView {
List {
Text("Hello, World")
}
.navigationBarTitle(Text("Items"))
.navigationBarItems(trailing: Button(action: {
print("Do Something")
}, label: {
Image(systemName: "plus")
.frame(width: 100, height: 50)
.contentShape(Rectangle())
.border(Color.red, width: 3)
.background(Color.gray)
}))
}
}
}
I hope this helps.
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.