.onPreferenceChange under GeometryReader does not update state variable - swiftui

Why the geometryreader always prints current index 1? The expected functionality is to print the shown object id when the user scrolls. But it does not change, what could be the issue? The current index should change when user scrolls without pressing the buttons.
struct SwiftUIView: View {
{
ScrollView {
ScrollViewReader { value in
Button("Go to next") {
withAnimation {
value.scrollTo((currentIndex), anchor: .top)
currentIndex += 1
print (currentIndex)
}
}
.padding()
Button("Go to previous") {
withAnimation {
value.scrollTo((currentIndex), anchor: .top)
currentIndex -= 1
print (currentIndex)
}
}
.padding()
VStack {
ForEach(0..<12, id: \.self) { obj in
PostCard(message:message1)
.onAppear{
print(">> added \(obj)")
current.append(obj)
}
.onDisappear {
current.removeAll { $0 == obj }
print("<< removed \(obj)")
}.id(obj)
}
}
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: -$0.frame(in: .named("scroll")).origin.y)
})
.onPreferenceChange(ViewOffsetKey.self) {_ in
if current.count > 0 {
currentIndex = current.sorted()[1]
print("current index \(String (currentIndex))")
}
}
}
}
}
}
}
}
}
}

Related

Animate text position simultaneously with text changing in SwiftUI

I want to animate the position of Expand button, but instead it appears right at the final position. There's a strange hack .transition(.scale) that fixes my problem, but I hope to see a better not hacky solution.
struct TextView: View {
#State var isExpanded = false
var body: some View {
VStack(spacing: 10) {
Text("This is the most recent test comment with a picture. Lörem ipsum berölogi kjolprotest. Trist gigade. Sms-livräddare grönt elcertifikat.")
.lineLimit(isExpanded ? nil : 2)
HStack {
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Spacer()
Text(isExpanded ? "Less" : "Expand")
}
// .transition(.scale)
}
}
.padding(.all)
.background(Color.yellow)
.cornerRadius(13)
.padding(.horizontal)
}
}
One option is to use a ZStack containing Buttons in both states, and use the .opacity modifier to hide or show them…
struct ContentView: View {
#State var isExpanded = false
var body: some View {
VStack(spacing: 10) {
Text("This is the most recent test comment with a picture. Lörem ipsum berölogi kjolprotest. Trist gigade. Sms-livräddare grönt elcertifikat.")
.lineLimit(isExpanded ? nil : 2)
HStack {
ZStack {
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Spacer()
Text("Less")
}
.opacity(isExpanded ? 1 : 0)
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Spacer()
Text("Expand")
}
.opacity(isExpanded ? 0 : 1)
}
}
}
.padding(.all)
.background(Color.yellow)
.cornerRadius(13)
.padding(.horizontal)
}
}
Or alternatively, use .matchedGeometryEffect
if isExpanded {
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Spacer()
Text("Less")
}
.matchedGeometryEffect(id: "button", in: namespace)
} else {
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Spacer()
Text("Expand")
}
.matchedGeometryEffect(id: "button", in: namespace)
}
Obviously there's some duplication so you would pull the Button out into a func or its own View struct.

SwiftUI DataStructure for ForEach.. (how to delete)

I'm working on an app which show images when tapping buttons.
I have a 4 buttons, and Each buttons has a action that add a image.
The problem I have is that when I remove by ontapGesture, The deleting order is not my want.
Ex) button 1 -> button 2 -> button 3 -> button 4 : Now I can see 4 images in vstack, plus, minus, multiply, divide.
But If I tap button 1 now, the plus image still there.
I think the problem arises because of array. What dataStructure should I use instead?
`
import SwiftUI
struct ContentView: View {
#State var array: [Int] = []
let systemDictionary: [Int : Image] = [
0 : Image(systemName: "plus.circle"),
1 : Image(systemName: "minus.circle"),
2 : Image(systemName: "multiply.circle"),
3 : Image(systemName: "divide.circle")
]
var body: some View {
VStack {
HStack {
Button {
array.append(0)
} label: {
Text("Button 0")
}
Button {
array.append(1)
} label: {
Text("Button 1")
}
Button {
array.append(2)
} label: {
Text("Button 2")
}
Button {
array.append(3)
} label: {
Text("Button 3")
}
}
ForEach(array.indices, id: \.self) { index in
systemDictionary[index]
.font(.system(size: 30))
.padding()
.onTapGesture {
array.remove(at: index)
}
}
}
}
}
`
I found out when I tap button 1 again, it delete the data in array[0].
But there is still array[0], because array always has index.
I understand! but I can not come up with data structure for images.
Using your current setup, you could try this approach, using the content of the array array[index] for the index into systemDictionary, and to limit the set of images to 4 only, use if !array.contains(0) { array.append(0) } as shown in the code:
struct ContentView: View {
#State var array: [Int] = []
let systemDictionary: [Int : Image] = [
0 : Image(systemName: "plus.circle"),
1 : Image(systemName: "minus.circle"),
2 : Image(systemName: "multiply.circle"),
3 : Image(systemName: "divide.circle")
]
var body: some View {
VStack {
HStack {
Button {
if !array.contains(0) { array.append(0) } // <-- here
} label: {
Text("Button 0")
}
Button {
if !array.contains(1) { array.append(1) }
} label: {
Text("Button 1")
}
Button {
if !array.contains(2) { array.append(2) }
} label: {
Text("Button 2")
}
Button {
if !array.contains(3) { array.append(3) }
} label: {
Text("Button 3")
}
}
ForEach(array.indices, id: \.self) { index in
systemDictionary[array[index]] // <-- here
.font(.system(size: 30))
.padding()
.onTapGesture {
array.remove(at: index)
}
}
}
}
}
EDIT-1: using a specific structure for the images:
struct Picture: Identifiable {
let id = UUID()
var image: Image
var label: String
var isVisible: Bool = false
}
struct ContentView: View {
#State var pictures: [Picture] = [
Picture(image: Image(systemName: "plus.circle"), label: "Button 0"),
Picture(image: Image(systemName: "minus.circle"), label: "Button 1"),
Picture(image: Image(systemName: "multiply.circle"), label: "Button 2"),
Picture(image: Image(systemName: "divide.circle"), label: "Button 3")
]
var body: some View {
VStack {
HStack {
ForEach($pictures) { $picture in
Button {
picture.isVisible = true
} label: {
Text(picture.label)
}
}
}
ForEach($pictures) { $picture in
if picture.isVisible {
picture.image
.font(.system(size: 30))
.padding()
.onTapGesture {
picture.isVisible = false
}
}
}
}
}
}

How to know which rectangle is user viewing?

The following scroll view works well. It has buttons to go to next and previous Rectangles. However, the user can scroll without the buttons as well (expected functionality). Then the currentIndex wont change, how to make the currentIndex change according to the rectangle that the user is viewing?
var body: some View {
var currentIndex: Int = 0
ScrollView {
ScrollViewReader { value in
Button("Go to next") {
withAnimation {
value.scrollTo((currentIndex), anchor: .top)
currentIndex += 1
print (currentIndex)
}
}
.padding()
Button("Go to previous") {
withAnimation {
value.scrollTo((currentIndex), anchor: .top)
currentIndex -= 1
print (currentIndex)
}
}
.padding()
ForEach(0..<100) { i in
ZStack {
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
Text (i)
.id(i)
.onAppear{ currentIndex = i
print (currentIndex) }
}
}
}
}}
Edits:
If I add .onAppear{ currentIndex = i, print (currentIndex) but neither it changes to the current object id and it does not get printed as well. What's the reason?

TabView disconnects when rotating to Landscape due to SwiftUI's re-render of parent-Views

Using Swift5.3.2, iOS14.4.1, XCode12.4,
As the following code shows, I am working with a quite complex TabView in Page-Mode in SwiftUI.
i.e. using iOS14's new possibility to show Pages:
.tabViewStyle(PageTabViewStyle())
Everything works.
Except, if I rotate my iPhone from Portrait to Landscape, the TabView disconnects and sets the selectedTab index to 0 (i.e. no matter where you scrolled to, rotating iPhone resets unwontedly to page 0).
The parent-View itself is in a complex View hierarchy. And one of the parent-View's of the TabView is updated during the TabView is shown (and swiped). And this might be the problem that the TabView gets re-rendered when rotating to Landscape.
What can I do to keep the TabView-Page during iPhone rotation ??
Here is the code:
import SwiftUI
struct PageViewiOS: View {
var body: some View {
ZStack {
Color.black
MediaTabView()
CloseButtonView()
}
}
}
And the MediaTabView at question:
struct MediaTabView: View {
#EnvironmentObject var appStateService: AppStateService
#EnvironmentObject var commService: CommunicationService
#State private var tagID = ""
#State private var selectedTab = 0
#State private var uniqueSelected = 0
#State private var IamInSwipingAction = false
var body: some View {
let myDragGesture = DragGesture(minimumDistance: 10)
.onChanged { _ in
IamInSwipingAction = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(5000)) {
IamInSwipingAction = false // workaround: because onEnded does not work...
}
}
.onEnded { _ in
IamInSwipingAction = false
}
TabView(selection: self.$selectedTab) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
switch appStateService.appState {
case .content(let tagID):
return list.tagId == tagID
default:
return false
}
}) {
if list.paths.count > 0 {
ForEach(list.paths.indices, id: \.self) { index in
ZoomableScrollView {
if let url = URL(fileURLWithPath: list.paths[index]){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
} else if url.containsVideo {
CustomPlayerView(url: url)
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.onAppear() {
if uniqueSelected != selectedTab {
uniqueSelected = selectedTab
if IamInSwipingAction && (commService.communicationRole == .moderatorMode) {
commService.send(thCmd: THCmd(key: .swipeID, sender: "", content: URL(fileURLWithPath: list.paths[index]).lastPathComponent))
}
}
}
}
} else {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
if Constants.TrihowAlbum.tagIdArrayTrihowAlbum.contains(tagID) {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
} else {
Text(LocalizedStringKey("TagNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
}
.onAppear() {
switch appStateService.appState {
case .content(let tagID):
self.tagID = tagID
default:
self.tagID = ""
}
}
.tabViewStyle(PageTabViewStyle())
.onTHComm_ReceiveCmd(service: commService) { (thCmd) in
switch thCmd.key {
case .swipeID:
if (commService.communicationRole == .moderatorMode) || (commService.communicationRole == .discoveryMode) {
selectTabFromCmdID(fileName: thCmd.content)
} else {
break
}
default:
break
}
}
.simultaneousGesture(myDragGesture)
}
}
extension MediaTabView {
private func selectTabFromCmdID(fileName: String) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
return list.tagId == tagID
}) {
if list.paths.count > 0 {
if let idx = list.paths.firstIndex(where: { (urlPath) -> Bool in
if let url = URL(string: urlPath) {
return url.lastPathComponent == fileName
} else { return false }
}) {
selectedTab = idx
}
}
}
}
}

How do I insert a row of buttons with a staggered, scale animation?

I'm attempting to recreate the following animation from the Castro app...
(The GIF is slowed down so you can see the effect)
As you can see in the GIF above, you have a row of buttons that appear when the cell is tapped. Each button has a zoom-in and zoom-out effect. The animations are staggered, such that the first button finishes first and the last button finishes last.
What I've tried...
struct SwiftUIView: View {
#State var show: Bool = false
var body: some View {
VStack {
Button(action: {
withAnimation {
show.toggle()
}
}, label: {
Text("Button")
})
HStack {
if show {
Button(action: {}, label: { Image(systemName: "circle") })
.transition(.scale)
Button(action: {}, label: { Image(systemName: "circle") })
.transition(.scale)
Button(action: {}, label: { Image(systemName: "circle") })
.transition(.scale)
}
}
}
}
}
As you can see in the image above... Each button does zoom in, but only as the view is removed. Also, I don't know how to stagger the animation.
Try the following:
struct ContentView: View {
#State var show = false
#State var showArray = Array(repeating: false, count: 3)
var body: some View {
VStack {
Button(action: toggleButtons) {
Text("Button")
}
HStack {
ForEach(showArray.indices, id: \.self) { index in
self.circleView(for: index)
}
}
}
}
#ViewBuilder
func circleView(for index: Int) -> some View {
if show {
ZStack {
Image(systemName: "circle")
.opacity(.leastNonzeroMagnitude)
.animation(nil)
if showArray[index] {
Image(systemName: "circle")
.transition(.scale)
}
}
}
}
func toggleButtons() {
showArray.indices.forEach { index in
withAnimation {
self.show = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(index)) {
withAnimation {
self.showArray[index].toggle()
if index == self.showArray.count - 1, self.showArray[index] == false {
self.show = false
}
}
}
}
}
}
It uses a little hack to align the views correctly - in ZStack there is a fake Image with almost no opacity.