SwiftUI: How to align view to HStack's subview? - view

I want to align the circle to the TileView "1"'s center at top left. Is there any other like centre constraint of UIView?
struct BoardView: View {
var body: some View {
ZStack {
VStack {
HStack {
TileView(number: 1)
TileView(number: 2)
}
HStack {
TileView(number: 3)
TileView(number: 4)
}
}
Circle()
.frame(width: 30, height: 30)
.foregroundColor(Color.red.opacity(0.8))
}
}
}

Here is possible approach using custom alignment guides
extension VerticalAlignment {
private enum VCenterAlignment: AlignmentID {
static func defaultValue(in dimensions: ViewDimensions) -> CGFloat {
return dimensions[VerticalAlignment.center]
}
}
static let vCenterred = VerticalAlignment(VCenterAlignment.self)
}
extension HorizontalAlignment {
private enum HCenterAlignment: AlignmentID {
static func defaultValue(in dimensions: ViewDimensions) -> CGFloat {
return dimensions[HorizontalAlignment.center]
}
}
static let hCenterred = HorizontalAlignment(HCenterAlignment.self)
}
struct BoardView: View {
var body: some View {
ZStack(alignment: Alignment(horizontal: .hCenterred, vertical: .vCenterred)) {
VStack {
HStack {
TileView(number: 1)
.alignmentGuide(.vCenterred) { $0[VerticalAlignment.center] }
.alignmentGuide(.hCenterred) { $0[HorizontalAlignment.center] }
TileView(number: 2)
}
HStack {
TileView(number: 3)
TileView(number: 4)
}
}
Circle()
.frame(width: 30, height: 30)
.foregroundColor(Color.red.opacity(0.8))
.alignmentGuide(.vCenterred) { $0[VerticalAlignment.center] }
.alignmentGuide(.hCenterred) { $0[HorizontalAlignment.center] }
}
}
}
Test module on GitHub

Using GeometryReader and preference
GeometryReader - let us to get geometryProxy of a view ( according to the parent view / space provide by parent )
Note:- child view stays at their own (parent cant force them position or size)
.preference(key:,value:) - this modifier let us get some values( geometryProxy ) out of the view. so that we can access child's geometry proxy from parent as well.
comment below for any question about these topics.
here is the code,
(plus point - you can animate your circle's position easily.(by adding .animation() modifier. Ex- if you want your circle move when user touch on a title)
complete code
import SwiftUI
struct ContentView: View {
#State var centerCoordinateOfRectangele: MyPreferenceData = MyPreferenceData(x: 0, y: 0)
var body: some View {
ZStack {
VStack {
HStack {
GeometryReader { (geometry) in
TileView(number: 1)
.preference(key: MyPreferenceKey.self, value: MyPreferenceData(x: geometry.frame(in: .named("spaceIWantToMoveMyView")).midX, y: geometry.frame(in: .named("spaceIWantToMoveMyView")).midY))
}
TileView(number: 2)
}.onPreferenceChange(MyPreferenceKey.self) { (value) in
self.centerCoordinateOfRectangele = value
}
HStack {
TileView(number: 3)
TileView(number: 4)
}
}
Circle()
.frame(width: 30, height: 30)
.foregroundColor(Color.red.opacity(0.8))
.position(x: centerCoordinateOfRectangele.x, y: centerCoordinateOfRectangele.y)
}
.coordinateSpace(name: "spaceIWantToMoveMyView") //to get values relative to this layout
// watch where i have use this name
}
}
struct TileView: View{
let number: Int
var body: some View{
GeometryReader { (geometry) in
RoundedRectangle(cornerRadius: 30)
}
}
}
struct MyPreferenceData:Equatable {
let x: CGFloat
let y: CGFloat
}
struct MyPreferenceKey: PreferenceKey {
typealias Value = MyPreferenceData
static var defaultValue: MyPreferenceData = MyPreferenceData(x: 0, y: 0)
static func reduce(value: inout MyPreferenceData, nextValue: () -> MyPreferenceData) {
value = nextValue()
}
}
Code explanation ->
data model which I want to grab from the child view
struct MyPreferenceData:Equatable {
let x: CGFloat
let y: CGFloat
}
Key
struct MyPreferenceKey: PreferenceKey {
typealias Value = MyPreferenceData
static var defaultValue: MyPreferenceData = MyPreferenceData(x: 0, y: 0)
static func reduce(value: inout MyPreferenceData, nextValue: () -> MyPreferenceData) {
value = nextValue()
}
}
defaultValue - SwiftUI use this, when there are no explicit value set
reduce() - getCalled when SwiftUI want to give values (we can use .preference() modifier on many views with the same key)
we add the .preference() modifier which we want to get values

Related

Simultaneous matchedGeometryEffect and rotation3DEffect

I want to animate a card, which flies from top half of screen to the bottom half and flips during the fly.
I control the flipping logic using custom .cardify modifier. It seems working alone, e.g. when I flip a card by onTapGesture { withAnimation { ... } }.
I also made a card fly from top of screen to the bottom and vice versa using matchedGeometryEffect.
But, when I tap card it flies without rotation.
I tried to apply .transition(.assymetric(...)) for both if-branches (see code below) but it did not help.
So, the code
import SwiftUI
struct ContentView: View {
#State var cardOnTop = true
#State var theCard = Card(isFaceUp: true)
#Namespace private var animationNameSpace
var body: some View {
VStack {
if cardOnTop {
CardView(card: theCard)
.matchedGeometryEffect(id: 1, in: animationNameSpace)
.onTapGesture {
withAnimation {
theCard.toggle()
cardOnTop.toggle() // comment me to test flipping alone
}
}
Color.white
} else {
Color.white
CardView(card: theCard)
.matchedGeometryEffect(id: 1, in: animationNameSpace)
.onTapGesture {
withAnimation {
theCard.toggle()
cardOnTop.toggle()
}
}
}
}
.padding()
}
struct Card {
var isFaceUp: Bool
mutating func toggle() {
isFaceUp.toggle()
}
}
struct CardView: View {
var card: Card
var body: some View {
Rectangle()
.frame(width: 100, height: 50)
.foregroundColor(.red)
.cardify(isFaceUp: card.isFaceUp)
}
}
}
/* Cardify ViewModifier */
struct Cardify: ViewModifier, Animatable {
init(isFaceUp: Bool){
rotation = isFaceUp ? 0 : 180
}
var rotation: Double // in degrees
var animatableData: Double {
get { return rotation }
set { rotation = newValue }
}
func body(content: Content) -> some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
if rotation < 90 {
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: DrawingConstants.lineWidth).foregroundColor(.gray)
} else {
shape.fill().foregroundColor(.gray)
}
content
.opacity(rotation < 90 ? 1 : 0)
}
.rotation3DEffect(Angle.degrees(rotation), axis: (0, 1, 0))
}
private struct DrawingConstants {
static let cornerRadius: CGFloat = 15
static let lineWidth: CGFloat = 2
}
}
extension View {
func cardify(isFaceUp: Bool) -> some View {
return self.modifier(Cardify(isFaceUp: isFaceUp))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
How can I make the card flying with rotation?
Also, I found that this works (if put in the body of VStack alone, no if-branches)
CardView(card: theCard)
.offset(x: 0, y: cardOnTop ? 0 : 100)
.onTapGesture {
withAnimation {
theCard.toggle()
cardOnTop.toggle() // comment me to test flipping alone
}
}
but in my app I have a deck of flipped down cards, which are dealt to board flipping up. I tried to model the behavior by if-branch in the code of ContentView above (CardViews appear and dissapear).
Here is a solution purely with .transition using a custom transition:
struct ContentView: View {
#State var cardOnTop = true
var body: some View {
VStack(spacing:0) {
if cardOnTop {
RoundedRectangle(cornerRadius: 15)
.fill(.blue)
.transition(.rotate3D(direction: 1).combined(with: .move(edge: .bottom)))
Color.clear
} else {
Color.clear
RoundedRectangle(cornerRadius: 15)
.fill(.green)
.transition(.rotate3D(direction: -1).combined(with: .move(edge: .top)))
}
}
.onTapGesture {
withAnimation(.easeInOut(duration: 2)) {
cardOnTop.toggle()
}
}
.padding()
}
}
/* Transition Modifier */
extension AnyTransition {
static func rotate3D(direction: Double) -> AnyTransition {
AnyTransition.modifier(
active: Rotate3DModifier(value: 1, direction: direction),
identity: Rotate3DModifier(value: 0, direction: direction))
}
}
struct Rotate3DModifier: ViewModifier {
let value: Double
let direction: Double
func body(content: Content) -> some View {
content
.rotation3DEffect(Angle(degrees: 180 * value * direction), axis: (x: 0, y: 1, z: 0))
.opacity(1 - value)
}
}
The rotation isn't triggered because you remove the card and insert another one into the view, so only the .matchedGeometryEffect remains.
What you want is one card that stays in view so it can rotate, but also change its position.
You can achieve this e.g. with .offset. The GeometryReader is just to find the right offset value.
var body: some View {
GeometryReader { geo in
VStack {
CardView(card: theCard)
.offset(x: 0, y: cardOnTop ? 0 : geo.size.height / 2)
.onTapGesture {
withAnimation {
cardOnTop.toggle()
theCard.toggle()
}
}
Color.clear
}
}
.padding()
}

SwiftUI - How to animate components corresponding to array elements?

I have an HStack of circles in SwiftUI, and the number of circles is determined based on the length of an array, like this:
#State var myArr = [...]
...
ScrollView(.horizontal) {
HStack {
ForEach(myArr) { item in
Circle()
//.frame(...)
//.animation(...) I tried this, it didn't work
}
}
}
Then I have a button that appends an element to this array, effectively adding a circle to the view:
Button {
myArr.append(...)
} label: {
...
}
The button works as intended, however, the new circle that is added to the view appears very abruptly, and seems choppy. How can I animate this in any way? Perhaps it slides in from the side, or grows from a very small circle to its normal size.
You are missing transition, here is what you looking:
struct ContentView: View {
#State private var array: [Int] = Array(0...2)
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(array, id:\.self) { item in
Circle()
.frame(width: 50, height: 50)
.transition(AnyTransition.scale)
}
}
}
.animation(.default, value: array.count)
Button("add new circle") {
array.append(array.count)
}
Button("remove a circle") {
if array.count > 0 {
array.remove(at: array.count - 1)
}
}
}
}
a version with automatic scroll to the last circle:
struct myItem: Identifiable, Equatable {
let id = UUID()
var size: CGFloat
}
struct ContentView: View {
#State private var myArr: [myItem] = [
myItem(size: 10),
myItem(size: 40),
myItem(size: 30)
]
var body: some View {
ScrollViewReader { scrollProxy in
VStack(alignment: .leading) {
Spacer()
ScrollView(.horizontal) {
HStack {
ForEach(myArr) { item in
Circle()
.id(item.id)
.frame(width: item.size, height: item.size)
.transition(.scale)
}
}
}
.animation(.easeInOut(duration: 1), value: myArr)
Spacer()
Button("Add One") {
let new = myItem(size: CGFloat.random(in: 10...100))
myArr.append(new)
}
.onChange(of: myArr) { _ in
withAnimation {
scrollProxy.scrollTo(myArr.last!.id, anchor: .trailing)
}
}
.frame(maxWidth: .infinity, alignment: .center)
}
.padding()
}
}
}

Show a sheet in response to a drop

I'm implementing drag and drop, and have a case where I need the user to decide what to do in response to a drop. So I want to bring up a sheet to ask the user for input. The problem is that the sheet doesn't appear until I drag another item to the same view. This does make sense, so I'm looking for a way to handle this differently.
The current approach looks like this (simplified):
struct SymbolInfo {
enum SymbolType {
case string, systemName
}
var type: SymbolType
var string: String
}
struct MyView: View, DropDelegate {
#State var sheetPresented = false
#State var droppedText = ""
static let dropTypes = [UTType.utf8PlainText]
var textColor = NSColor.white
private var frameRect: CGRect = .null
private var contentPath: Path = Path()
private var textRect: CGRect = .null
#State private var displayOutput: SymbolInfo
#State private var editPopoverIsPresented = false
// There's an init to set up the display output, the various rects and path
var body: some View {
ZStack(alignment: stackAlignment) {
BackgroundView() // Draws an appropriate background
.frame(width: frameRect.width, height: frameRect.height)
if displayOutput.type == .string {
Text(displayOutput.string)
.frame(width: textRect.width, height: textRect.height, alignment: .center)
.foregroundColor(textColour)
.font(displayFont)
.allowsTightening(true)
.lineLimit(2)
.minimumScaleFactor(0.5)
}
else {
Image(systemName: displayOutput.string)
.frame(width: textRect.width, height: textRect.height, alignment: .center)
.foregroundColor(textColour)
.minimumScaleFactor(0.5)
}
}
.onAppear {
// Retrieve state information from the environment
}
.focusable(false)
.allowsHitTesting(true)
.contentShape(contentPath)
.onHover { entered in
// Populates an inspector
}
.onTapGesture(count: 2) {
// Handle a double click
}
.onTapGesture(count: 1) {
// Handle a single click
}
.popover(isPresented: $editPopoverIsPresented) {
// Handles a popover for editing data
}
.onDrop(of: dropTypes, delegate: self)
.sheet(sheetPresented: $sheetPresented, onDismiss: sheetReturn) {
// sheet to ask for the user's input
}
}
func sheetReturn() {
// act on the user's input
}
func performDrop(info: DropInfo) -> Bool {
if let item = info.itemProviders(for: dropTypes).first {
item.loadItem(forTypeIdentifier: UTType.utf8PlainText.identifier, options: nil) { (textData, error) in
if let textData = String(data: textData as! Data, encoding: .utf8) {
if (my condition) {
sheetIsPresented = true
droppedText = textData
}
else {
// handle regular drop
}
}
}
return true
}
return false
}
}
So my reasoning is that the drop sets sheetPresented to true, but then it doesn't get acted on until the view is rebuilt, such as on dragging something else to it. But I'm still new to SwiftUI, so I may be incorrect.
Is there a way to handle this kind of interaction that I haven't found?
I never was able to exactly reproduce the problem, but the issue related to trying to have more than one kind of sheet that could be shown, depending on conditions. The solution was to break up the original view into a family of views that encapsulated the different behaviours, and show the appropriate one rather than try to make one view do everything.
I won't show the whole code, since it's too deeply embedded in the app, but here's a demo app that works correctly:
import SwiftUI
import UniformTypeIdentifiers
#main
struct DragAndDropSheetApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
HStack() {
TargetView(viewType: .normal, viewText: "A")
.frame(width: 40, height: 40, alignment: .top)
TargetView(viewType: .protected, viewText: "B")
.frame(width: 40, height: 40, alignment: .top)
TargetView(viewType: .normal, viewText: "C")
.frame(width: 40, height: 40, alignment: .top)
TargetView(viewType: .protected, viewText: "D")
.frame(width: 40, height: 40, alignment: .top)
}
.padding()
}
}
enum ViewType {
case normal, protected
}
struct TargetView: View, DropDelegate {
#State private var sheetPresented = false
#State var viewType: ViewType
#State var viewText: String
#State private var dropText = ""
#State private var dropType: DropActions = .none
static let dropTypes = [UTType.utf8PlainText]
var body: some View {
ZStack(alignment: .center) {
Rectangle()
.foregroundColor(viewType == .normal ? .blue : .red)
Text(viewText)
.foregroundColor(.white)
.frame(width: nil, height: nil, alignment: .center)
}
.focusable(false)
.allowsHitTesting(true)
.onDrop(of: TargetView.dropTypes, delegate: self)
.sheet(isPresented: $sheetPresented, onDismiss: handleSheetReturn) {
ProtectedDrop(isPresented: $sheetPresented, action: $dropType)
}
}
func handleSheetReturn() {
switch dropType {
case .append:
viewText += dropText
case .replace:
viewText = dropText
case .none:
// Nothing to do
return
}
}
func performDrop(info: DropInfo) -> Bool {
if let item = info.itemProviders(for: TargetView.dropTypes).first {
item.loadItem(forTypeIdentifier: UTType.utf8PlainText.identifier, options: nil) { textData, error in
if let textData = String(data: textData as! Data, encoding: .utf8) {
if viewType == .normal {
viewText = textData
}
else {
dropText = textData
sheetPresented = true
}
}
}
return true
}
return false
}
}
enum DropActions: Hashable {
case append, replace, none
}
struct ProtectedDrop: View {
#Binding var isPresented: Bool
#Binding var action: DropActions
var body: some View {
VStack() {
Text("This view is protected. What do you want to do?")
Picker("", selection: $action) {
Text("Append the dropped text")
.tag(DropActions.append)
Text("Replace the text")
.tag(DropActions.replace)
}
.pickerStyle(.radioGroup)
HStack() {
Spacer()
Button("Cancel") {
action = .none
isPresented.toggle()
}
.keyboardShortcut(.cancelAction)
Button("OK") {
isPresented.toggle()
}
.keyboardShortcut(.defaultAction)
}
}
.padding()
}
}

How to display an image where the user taps? SwiftUI

I am making a mining tapping game and I want to display a hammer wherever the user taps.
I mean, wherever the user taps the hammer image will stay on for one second.
Is there a way to do it?
My example code is below:
struct Level1: View {
#State var tapScore = 0
#State var showingMinedHammer = false
func showMinedHammer() {
self.showingMinedHammer = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.showingMinedHammer = false
}
}
func mine() {
tapScore += 1
showMinedHammer()
}
var body: some View {
GeometryReader { geometryProxy in
ZStack {
Image("mine1").resizable().frame(width: UIScreen.main.bounds.height * 1.4, height: UIScreen.main.bounds.height)
.onTapGesture {
self.mine()
}
if self.showingMinedHammer {
Image(systemName: "hammer.fill")
.resizable()
.frame(width: 30, height: 30)
}
}
}.edgesIgnoringSafeArea(.all)
}
}
It just need to read location of tap and use it as position for hammer image, like below - all by SwiftUI
Tested with Xcode 11.4 / iOS 13.4
Here is modified only part
#State private var location = CGPoint.zero // < here !!
var body: some View {
GeometryReader { geometryProxy in
ZStack {
Image("mine1").resizable().frame(width: UIScreen.main.bounds.height * 1.4, height: UIScreen.main.bounds.height)
.gesture(DragGesture(minimumDistance: 0).onEnded { value in
self.location = value.location // < here !!
self.mine()
})
if self.showingMinedHammer {
Image(systemName: "hammer.fill")
.resizable()
.frame(width: 30, height: 30)
.position(self.location) // < here !!
}
}
}.edgesIgnoringSafeArea(.all)
}
To get the location of where you tapped, you can do something like this:
import SwiftUI
struct ContentView: View {
#State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
var body: some View {
ZStack{
GetTapLocation {
// tappedCallback
location in
self.points.append(location)
print(self.points)
}
}
}
}
struct GetTapLocation:UIViewRepresentable {
var tappedCallback: ((CGPoint) -> Void)
func makeUIView(context: UIViewRepresentableContext<GetTapLocation>) -> UIView {
let v = UIView(frame: .zero)
let gesture = UITapGestureRecognizer(target: context.coordinator,
action: #selector(Coordinator.tapped))
v.addGestureRecognizer(gesture)
return v
}
class Coordinator: NSObject {
var tappedCallback: ((CGPoint) -> Void)
init(tappedCallback: #escaping ((CGPoint) -> Void)) {
self.tappedCallback = tappedCallback
}
#objc func tapped(gesture:UITapGestureRecognizer) {
let point = gesture.location(in: gesture.view)
self.tappedCallback(point)
}
}
func makeCoordinator() -> GetTapLocation.Coordinator {
return Coordinator(tappedCallback:self.tappedCallback)
}
func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<GetTapLocation>) {
}
}
There has to be a simpler implementation, but until then you can get the location where you tapped. I hope that helps :)

How can you transition the size of a view back and forth in SwiftUI?

Edit: With help of Asperi I decided to rewrite the description to better clarify the question with easy copy+paste code.
Expected behavior on all tests: The red rectangle will animate it's size from zero to the size of the parent view when the Present button in the top right corner is tapped. When Present is tapped again, the red rectangle will shrink from the size of the parent view to zero.
TEST #1 PROPERTY STATE CHANGE
Actual behavior:
Works as expected.
Code:
struct ContentView: View {
#State private var presentRedBox = false
var body: some View {
NavigationView {
GeometryReader { proxy in
ZStack {
// ------
Rectangle().fill(Color.red)
.frame(
width: self.presentRedBox ? proxy.size.width : 0.0,
height: self.presentRedBox ? proxy.size.height : 0.0
)
// ------
}
}.animation(.default)
.navigationBarItems(trailing: Button("Present") { self.presentRedBox.toggle() })
.navigationBarTitle(Text(""), displayMode: .inline)
}
}
}
TEST #2 ANIMATABLE/VIEW MODIFIER USING PROPERTY STATE CHANGE
Actual behavior:
Works as expected.
Code:
extension AnyTransition {
static func sizeTransition(from: CGSize, to: CGSize) -> AnyTransition {
.modifier(
active: SizeTransition(size: from),
identity: SizeTransition(size: to)
)
}
}
struct SizeTransition: AnimatableModifier {
var size: CGSize
var animatableData: AnimatablePair<cgfloat, cgfloat=""> {
get { AnimatablePair(size.width, size.height) }
set {
size.width = newValue.first
size.height = newValue.second
}
}
func body(content: Content) -> some View {
print(size)
return content.frame(
width: size.width,
height: size.height
)
}
}
struct ContentView: View {
#State private var presentRedBox = false
var body: some View {
NavigationView {
GeometryReader { proxy in
ZStack {
// ------
Rectangle().fill(Color.red)
.modifier(
SizeTransition(
size: self.presentRedBox ? proxy.size : .zero
)
)
// ------
}
}.animation(.default)
.navigationBarItems(trailing: Button("Present") { self.presentRedBox.toggle() })
.navigationBarTitle(Text(""), displayMode: .inline)
}
}
}
TEST #3 ANIMATABLE/VIEW MODIFIER WITH TRANSITION
Actual behavior:
The red rectanble will animate in as expected. However(!) it will NOT animate out but disappear immediately, although the log shows the correct values.
Log Animating In
(0.0, 0.0)
(1.8118343353271484, 3.3873424530029297)
(7.392631530761719, 13.821006774902344)
(16.9350643157959, 31.66120719909668)
(30.5800838470459, 57.17146110534668)
(48.38059616088867, 90.45067977905273)
(70.25803184509277, 131.35197257995605)
(95.95654678344727, 179.39702224731445)
(124.99998664855957, 233.6956272125244)
(156.67254066467285, 292.90953254699707)
(190.03098106384277, 355.27531242370605)
(223.97296714782715, 418.73206901550293)
(257.33140754699707, 481.0978488922119)
(289.00356674194336, 540.3110160827637)
(318.04700660705566, 594.6096210479736)
(343.7447319030762, 642.6531944274902)
(365.6217727661133, 683.5537490844727)
(383.42189025878906, 716.8322296142578)
(397.06651496887207, 742.3417453765869)
(406.60855293273926, 760.1812076568604)
(412.18856048583984, 770.613395690918)
(414.0, 774.0)
Log Animating Out
(413.61268043518066, 773.2758808135986)
(410.07547760009766, 766.6628494262695)
(402.6749496459961, 752.8270797729492)
(391.2381649017334, 731.4452648162842)
(375.6612854003906, 702.3232727050781)
(355.94628524780273, 665.4647941589355)
(332.24832916259766, 621.1599197387695)
(304.9215717315674, 570.070764541626)
(274.5523223876953, 513.2934722900391)
(241.9665470123291, 452.3722400665283)
(208.19354438781738, 389.231409072876)
(174.37908554077148, 326.0130729675293)
(141.67486381530762, 264.870397567749)
(111.12004852294922, 207.74617767333984)
(83.55758285522461, 156.21635055541992)
(59.59075355529785, 111.40880012512207)
(39.58871841430664, 74.01369094848633)
(23.71967124938965, 44.34547233581543)
(11.994667053222656, 22.42481231689453)
(4.315790176391602, 8.06865119934082)
(0.5136623382568359, 0.9603252410888672)
(0.0, 0.0)
Code:
extension AnyTransition {
static func sizeTransition(from: CGSize, to: CGSize) -> AnyTransition {
.modifier(
active: SizeTransition(size: from),
identity: SizeTransition(size: to)
)
}
}
struct SizeTransition: AnimatableModifier {
var size: CGSize
var animatableData: AnimatablePair<cgfloat, cgfloat=""> {
get { AnimatablePair(size.width, size.height) }
set {
size.width = newValue.first
size.height = newValue.second
}
}
func body(content: Content) -> some View {
print(size)
return content.frame(
width: size.width,
height: size.height
)
}
}
struct ContentView: View {
#State private var presentRedBox = false
var body: some View {
NavigationView {
GeometryReader { proxy in
ZStack {
// ------
if self.presentRedBox {
Rectangle().fill(Color.red)
.transition(
.modifier(
active: SizeTransition(size: .zero),
identity: SizeTransition(size: proxy.size)
)
)
}
// ------
}
}.animation(.default)
.navigationBarItems(trailing: Button("Present") { self.presentRedBox.toggle() })
.navigationBarTitle(Text(""), displayMode: .inline)
}
}
}
TEST #4 ANIMATABLE/VIEW MODIFIER WITH TRANSITION FOR OPACITY
Expected behavior:
The red rectangle will animate it's opacity from zero (hidden) to one (visible) when the Present button in the top right corner is tapped. When Present is tapped again, the red rectangle will hide from one (visible) to zero (hidden).
Actual behavior:
Works as expected.
Code:
extension AnyTransition {
static func sizeTransition(from: CGSize, to: CGSize) -> AnyTransition {
.modifier(
active: SizeTransition(size: from),
identity: SizeTransition(size: to)
)
}
}
struct SizeTransition: AnimatableModifier {
var size: CGSize
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get { AnimatablePair(size.width, size.height) }
set {
size.width = newValue.first
size.height = newValue.second
}
}
func body(content: Content) -> some View {
print(size)
return content.opacity(Double(size.width))
}
}
struct ContentView: View {
#State private var presentRedBox = false
var body: some View {
NavigationView {
GeometryReader { proxy in
ZStack {
// ------
if self.presentRedBox {
Rectangle().fill(Color.red)
.transition(
.modifier(
active: SizeTransition(size: .zero),
identity: SizeTransition(size: CGSize(width: 1.0, height: 1.0))
)
)
}
// ------
}
}.animation(.default)
.navigationBarItems(trailing: Button("Present") { self.presentRedBox.toggle() })
.navigationBarTitle(Text(""), displayMode: .inline)
}
}
}
It is not exactly a solution for what you requested, I know, but under some circumstances can be useful, so I decided to post it.
Note: I started with your previous post code, so might be a bit not aligned with this post.
At first proposed alternate - only animation based.
struct TestReverseTransitions: View {
#State private var showRedBox = false
var body: some View {
VStack {
Button("Tap") { self.showRedBox.toggle() }
RedBox()
.modifier(SizeAnimation(size: showRedBox ?
CGSize(width: 200.0, height: 200.0) : .zero))
}.animation(.default)
}
}
The animatable modifier is used the same from investigation provided below.
Transitions investigation:
Actually I think this might be a bug, but can be a transition engine limitation, because transitions based on effects, but here is just change of physical view frame, while view is in fact already removed. So 50/50... maybe worth reporting feedback to Apple.
Here is why...
I use animatable modifier to make frame change explicitly via animatable data and as it seen on Demo2 debug log, frame is really animatable on box removed, but appearance is not, however button is moved as it should. Bug? Maybe.
Code for this case:
extension AnyTransition {
static func size(from: CGSize, to: CGSize) -> AnyTransition {
AnyTransition.modifier(
active: SizeAnimation(size: from),
identity: SizeAnimation(size: to)
)
}
}
struct SizeAnimation: AnimatableModifier {
var size: CGSize
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get { AnimatablePair(size.width, size.height) }
set {
size.width = newValue.first
size.height = newValue.second
}
}
func body(content: Content) -> some View {
// print(size) // << uncomment for log sizes !!!
return content.frame(width: size.width, height: size.height)
}
}
struct TestReverseTransitions: View {
#State private var showRedBox = false
var body: some View {
VStack {
Button("Tap") { self.showRedBox.toggle() }
if self.showRedBox {
RedBox()
.transition(
.size(from: CGSize.zero, to: CGSize(width: 200.0, height: 200.0))
)
}
}.animation(.default)
}
}
struct RedBox: View {
var body: some View {
Rectangle().fill(Color.red)
}
}

Resources