This is probably a rookie mistake, but I'm trying to create an "expandable" frame in SwiftUI, i.e. a frame that could be extended via a draggable "slider". I'm thinking that the easiest way could be specifying a frame, and a rectangle as "slider" to drag.
Here's an MRE:
import SwiftUI
fileprivate enum Constants {
static let radius: CGFloat = 16
static let indicatorHeight: CGFloat = 6
static let indicatorWidth: CGFloat = 60
}
struct ContentView: View {
#GestureState private var dragAmount = CGSize.zero
private var frameheight : CGFloat = 200
var body: some View {
VStack{
Text("Hello, world!")
.frame(height: frameheight + dragAmount.height)
Divider()
RoundedRectangle(cornerRadius: Constants.radius)
.fill(Color.secondary)
.frame(
width: Constants.indicatorWidth,
height: Constants.indicatorHeight
)
.gesture(
DragGesture().updating($dragAmount) { value, state, transaction in
state = value.translation
print("Height: \(state), drag Amount: \(dragAmount)")
}
)
Divider()
Text("Hello, world!")
Text("Hello, world!")
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Looks ok... but it doesn't work. When I move the mouse up and down, my "print" statement indicates that dragAmount changes, but my frame size doesn't. Interestingly, changing "+ dragAmount.height" to " + dragAmount.width" does change the height, but only temporarily... and only if you drag the rectangle left/right.
Any insights why? And, of course, suggestions how to fix are welcome! :)
Thanks, Philipp
Related
Background
Using SwiftUI, I want to implement tutorial(Coach marks) functionalities for some view( like button, text, image and so on) which was defined without specifying origin and sizes
for modifier frame. especially such functionalities will be asked for several difference screen.
My Test Code
1. Definition of PreferenceKey
struct MyPreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
main view
struct ContentView: View {
#State var rating : Int = 3
var body: some View {
GeometryReader { geometry in
let origin = geometry.frame(in: .global).origin
let size = geometry.frame(in: .global).size
VStack {
HStack{
Text("Test1") // for testing following target,just add some view hierachy
.padding()
.background(Color.yellow)
VStack{
Text("test2")
.padding()
.background(Color.purple)
Text("") // here is my target, i want to get the origin of this view
.frame(width: 100, height: 200) // just for having spece to show value of frame
.background(Color.green)
.overlay(
GeometryReader { proxy in
let frm = proxy.frame(in: .global)
let size = proxy.size
Color.clear.preference(key: MyPreferenceKey.self,value: proxy.frame(in: .global))
Text(verbatim: "X=\(String(format: "%.2f", frm.midX)) y=\(String(format: "%.2f", frm.midY) ) width=\(String(format: "%.2f", frm.width)) height=\(String(format: "%.2f", frm.height))")
}
)
}
}
.onPreferenceChange(MyPreferenceKey.self){
print("\($0)")
}
}
.frame(width: 200, height: 300, alignment: .center)
}
}
}
3. Ran result (Xcode)
4.Ran result (Log)
(89.83333333333331, 127.16666666666669, 100.0, 200.0)
5. Problem
the view with green background says it's origin.x is 139, but logs printed above is 89 in modifier onPreferenceChange.
My Question:
how can i get real origin.x with 139 using like modifier onPreferenceChange.
is there a way to get value shown by above Text view
thanks.
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()
}
}
It seems that Transition.slide and Transition.move animations are broken. Using AnyTransition.slide.animation() does not animate. It requires an additional .animation() modifier. This is a problem, since often I want to animate only the slide and nothing else.
Here is a demo. The goal is to have a sliding animation without animating the black ball. The .scale and .opacity animations work just fine. But .slide and .move either do not animate, or they animate the black ball.
import SwiftUI
struct ContentView: View {
#State var screens: [Color] = [Color.red]
var body: some View {
ZStack {
ForEach(screens.indices, id: \.self) { i -> AnyView in
let screen = screens[i]
return AnyView(
Screen(color: screen)
.zIndex(Double(i))
// Try uncommenting these lines one by one. The scale and opacity animations work fine. But move and slide animate only when the additional .animation modifier is uncommented below.
// This is a problem, since uncommenting the standalone .animation modifier, also animates everything inside the Screen() view, which is not what I want.
// .transition(AnyTransition.scale.animation(.linear(duration: 2)))
// .transition(AnyTransition.opacity.animation(.linear(duration: 2)))
// .transition(AnyTransition.move(edge: .leading).animation(.linear(duration: 2)))
.transition(AnyTransition.slide.animation(.linear(duration: 2)))
// .animation(.linear(duration: 2))
)
}
VStack(spacing: 50) {
Text("Screens Count: \(screens.count)")
Button("prev") {
removeScreen()
}
.padding()
.background(Color.white)
Button("next") {
addScreen()
}
.padding()
.background(Color.white)
}
.zIndex(1000)
}
}
func addScreen() {
let colors = [Color.red, Color.blue, Color.green, Color.yellow]
screens.append(colors[screens.count % colors.count])
}
func removeScreen() {
guard screens.count > 1 else { return }
screens.removeLast()
}
}
struct Screen: View {
static var width = UIScreen.main.bounds.width / 2 - 25
static var height = UIScreen.main.bounds.height / 2 - 25
var color: Color
#State var offset = CGSize(width: Self.width, height: -1 * Self.height)
var body: some View {
ZStack {
color
.frame(maxWidth: .infinity, maxHeight: .infinity)
Circle()
.frame(width: 50)
.offset(offset)
}
.onAppear { offset = CGSize(width: Self.width, height: Self.height) }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Any thoughts how to use slide and move animations without animating everything else inside the view?
Ok, I have a solution. It's not perfect, but it's workable. Putting a view inside a NavigationView somehow separates .animation modifiers:
struct Screen: View {
static var width = UIScreen.main.bounds.width / 2 - 25
static var height = UIScreen.main.bounds.height / 2 - 25
var color: Color
#State var offset = CGSize(width: Self.width, height: -1 * Self.height)
var body: some View {
NavigationView {
ZStack {
color
.frame(maxWidth: .infinity, maxHeight: .infinity)
Circle()
.frame(width: 50)
.offset(offset)
}
.onAppear { offset = CGSize(width: Self.width, height: Self.height) }
.animation(.none)
.navigationBarHidden(true)
}
}
}
This code shows the general behavior I'm trying to achieve, but how can I make this continuous so there's no apparent end to the image, with no gaps of white space and a smooth transition? AND - isn't there a better way to do this???
Thanks!
import SwiftUI
struct ContentView: View {
#State private var xVal: CGFloat = 0.0
#State private var timer = Timer.publish(every: 0.05, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Image("game_background_2") //image attached
.aspectRatio(contentMode: .fit)
.offset(x: xVal, y: 0)
.transition(.slide)
.padding()
.onReceive(timer) {_ in
xVal += 2
if xVal == 800 { xVal = 0 }
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here's a simple approach that could work. First use a GeometryReader to set the frame of the screen. Then add an HStack with 2 images inside. The first image is full size and the 2nd image is limited to the width of the screen. Finally, animate the HStack to move from the .trailing to the .leading edge of the screen frame. We set the animation .repeatForever so that it continues loops and autoReverses: false so that it restarts. When it restarts, however, it should be seamless because the restart position is identical to the 2nd image.
struct ImageBackgroundView: View {
#State var animate: Bool = false
let animation: Animation = Animation.linear(duration: 10.0).repeatForever(autoreverses: false)
var body: some View {
GeometryReader { geo in
HStack(spacing: -1) {
Image("game_background_2")
.aspectRatio(contentMode: .fit)
Image("game_background_2")
.aspectRatio(contentMode: .fit)
.frame(width: geo.size.width, alignment: .leading)
}
.frame(width: geo.size.width, height: geo.size.height,
alignment: animate ? .trailing : .leading)
}
.ignoresSafeArea()
.onAppear {
withAnimation(animation) {
animate.toggle()
}
}
}
}
I have a List made of cells, each containing an image, and a column of text, which I wish laid out in a specific way. Image on the left, taking up a quarter of the width. The rest of the space given to the text, which is left-aligned.
Here's the code I got:
struct TestCell: View {
let model: ModelStruct
var body: some View {
HStack {
Image("flag")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: UIScreen.main.bounds.size.width * 0.25)
VStack(alignment: .leading, spacing: 5) {
Text("Country: Moldova")
Text("Capital: Chișinău")
Text("Currency: Leu")
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
}
}
struct TestCell_Previews: PreviewProvider {
static var previews: some View {
TestCell()
.previewLayout(.sizeThatFits)
.previewDevice("iPhone 11")
}
}
And here are 2 examples:
As you can see, the height of the whole cell varies based on the aspect ratio of the image.
$1M question - How can we make the cell height hug the text (like in the second image) and not vary, but rather shrink the image in a scaleAspectFit manner inside the allocated rectangle
Note!
The text's height can vary, so no hardcoding.
Couldn't make it work with PreferenceKeys, as the cells will be part of a List, and there's some peculiar behaviour I'm trying to grasp around cell reusage, and onPreferenceChange not being called when 2 consecutive cells have the same height. To exhibit all this combined behaviour, make sure your model varies between cells when you test it.
Here is a possible solution, however it uses GeometryReader inside the background property of the VStack, to detect their height. That height is being applied to the Image then. I used SizePreferenceKey from this solution.
struct SizePreferenceKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: Value = .zero
static func reduce(value _: inout Value, nextValue: () -> Value) {
_ = nextValue()
}
}
struct ContentView6: View {
#State var childSize: CGSize = .zero
var body: some View {
HStack {
Image("image1")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: UIScreen.main.bounds.size.width * 0.25, height: self.childSize.height)
VStack(alignment: .leading, spacing: 5) {
Text("Country: Moldova")
Text("Capital: Chișinău")
Text("Currency: Leu")
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.background(
GeometryReader { proxy in
Color.clear.preference(key: SizePreferenceKey.self, value: proxy.size)
}
)
}
.onPreferenceChange(SizePreferenceKey.self) { preferences in
self.childSize = preferences
}
.border(Color.yellow)
}
}
Will look like this.. you can apply different aspect ratios for the Image of course.
This is what worked for me to constrain a color view to the height of text content in a cell:
A height reader view:
struct HeightReader: View {
#Binding var height: CGFloat
var body: some View {
GeometryReader { proxy -> Color in
update(with: proxy.size.height)
return Color.clear
}
}
private func update(with value: CGFloat) {
guard value != height else { return }
DispatchQueue.main.async {
height = value
}
}
}
You can then use the reader in a compound view as a background on the view you wish to constrain to, using a state object to update the frame of the view you wish to constrain:
struct CompoundView: View {
#State var height: CGFloat = 0
var body: some View {
HStack(alignment: .top) {
Color.red
.frame(width: 2, height: height)
VStack(alignment: .leading) {
Text("Some text")
Text("Some more text")
}
.background(HeightReader(height: $height))
}
}
}
struct CompoundView_Previews: PreviewProvider {
static var previews: some View {
CompoundView()
}
}
I have found that using DispatchQueue to update the binding is important.