animation lets view jump - ios

I created a list of cells which are collapsable. It works quite well, but the title headed jumps around while being animated, as you can see in this small video. It should stay put in place. What am I doing wrong? Thanks
A compilable code can be found here: https://gitlab.com/vikingosegundo/brighter-hue
// LightCell.swift
let rowHeight = 36.0
let collapsedCellHeight = 80.0
let uncollapsedCellHeight = 180.0
let material:Material = .thickMaterial
struct LightCell: View {
init(light: Light, rootHandler: #escaping (Message) -> ()) {
self.rootHandler = rootHandler
self.light = light
self.isOn = light.isOn
self.hue = light.hue
self.sat = light.saturation
self.bri = light.brightness
self.ct = Double(-light.ct)
self.interface = light.display
self.selectedMode = light.modes.contains(.hsb) ? .hsb : .ct
self.values = light.modes.contains(.hsb) ? .hsb(light.hue, light.saturation, light.brightness) : .ct(light.ct, light.brightness)
}
var body: some View {
cellLayout
.frame( height: isCollapsed ? collapsedCellHeight : uncollapsedCellHeight)
.modifier(Height(isCollapsed ? collapsedCellHeight : uncollapsedCellHeight))
.swipeActions(edge: .leading ) { toggleColorMode }
.swipeActions(edge: .trailing) { toggleInterface }
.listRowBackground( bgColor )
.tint(hsbColor(opacity: 0.5))
.padding(.top, 15)
.padding(.bottom, 15)
.buttonStyle(PlainButtonStyle())
.onChange(of:light.ct ) { ct = Double(-$0); values = .ct (-$0, light.brightness) }
.onChange(of:light.hue ) { hue = $0; values = .hsb(-$0, light.saturation, light.brightness) }
.onChange(of:light.saturation) { sat = $0; values = .hsb(light.hue, $0, light.brightness) }
.onChange(of:light.brightness) { bri = $0; values = .hsb(light.hue, light.saturation, $0 ) }
}
private let rootHandler: (Message) -> ()
private let light: Light
#EnvironmentObject
private var viewState: ViewState
#State private var values: Values
#State private var isOn : Bool
#State private var hue : Double
#State private var sat : Double
#State private var bri : Double
#State private var ct : Double
#State private var interface : Interface
#State private var startPoint : CGPoint?
#State private var currentPoint : CGPoint?
#State private var isCollapsed : Bool = true
#State private var selectedMode : Light.Mode
}
private
extension LightCell {
private
var cellLayout: some View {
VStack {
cellheader;
if light.modes.contains(.hsb) || light.modes.contains(.ct) {
switch (selectedMode, light.display) {
case (.ct, .slider):
Spacer() .isCollapsed(isCollapsed)
ctSliderRow.isCollapsed(isCollapsed)
case (.hsb, .slider):
Spacer() .isCollapsed(isCollapsed)
hueSliderRow.isCollapsed(isCollapsed);
satSliderRow.isCollapsed(isCollapsed)
case (_, .scrubbing):
scrubbingView.isCollapsed(isCollapsed)
}
}
briSliderRow
}
}
private
var toggleColorMode: some View {
HStack {
if !isCollapsed{
let indexedModes = light
.modes
.filter { $0 != selectedMode }
.enumerated()
.map { (idx: $0.offset, mode: $0.element) }
ForEach(indexedModes, id: \.idx) { i in
Button { selectedMode = i.mode } label: { image(for: i.mode) }
}}
}
}
private
var toggleInterface: some View {
HStack {
if !isCollapsed{
Button {
rootHandler(.lighting(.change(.display(light.display == .slider ? .scrubbing : .slider), on:light)))
} label: { Label("", systemImage:
light.display == .slider
? "slider.horizontal.below.rectangle"
: "slider.horizontal.3")
}}
}
}
private
var cellheader: some View {
HStack {
Button {
withAnimation { isCollapsed.toggle() }
} label: {
Image(systemName:"chevron.up")
.foregroundColor(hsbColor(opacity:0.75))
.contentShape(Rectangle())
.rotationEffect( .radians(isCollapsed ? .pi : 0.0))
}
titleLabel
.multilineTextAlignment(.leading)
Spacer()
toogle
}
.rowStyle(height:rowHeight)
}
private
var titleLabel: some View {
Text("\(light.name)")
.bold()
.dynamicTypeSize(.xLarge)
.fixedSize()
}
private
var toogle: some View {
Toggle("", isOn: $isOn)
.onChange(of: isOn) { _ in
switch (light.isOn, isOn) {
case (true, false): rootHandler(.lighting(.turn(light,.off)))
case (false, true): rootHandler(.lighting(.turn(light,.on )))
case ( _ , _ ): ()
}
}
}
private
var scrubbingKnob: some View {
HStack {
Spacer()
Circle()
.fill(
LinearGradient(gradient: Gradient(colors: [hsbColor(saturation:1.0, opacity: 0.5),
hsbColor(saturation:1.0, opacity: 0.5),
hsbColor(saturation:0.1, opacity: 0.5),
hsbColor(saturation:0.1, opacity: 0.5)]),
startPoint:.top, endPoint:.bottom)
)}
.frame(minWidth: 20, maxWidth: 20, minHeight:20)
.offset(x:(currentPoint?.x ?? 0) - (startPoint?.x ?? 0), y:(currentPoint?.y ?? 0) - (startPoint?.y ?? 0))
.gesture(
DragGesture(
minimumDistance: 5,
coordinateSpace: .global
).onChanged{ n in
if let startpoint = startPoint {
switch selectedMode {
case .hsb:setHueAndSaturationFor(
distanceX: ( n.location.x ) - (startpoint.x),
distanceY: ( n.location.y ) - (startpoint.y))
case .ct: setCTfor(
distanceX: ( n.location.x ) - (startpoint.x),
distanceY: ( n.location.y ) - (startpoint.y))
}
}
switch startPoint == nil {
case true : startPoint = n.location
case false: currentPoint = n.location
}}
.onEnded { _ in startPoint = nil; currentPoint = nil } ).padding(.bottom, 20)
}
private
var scrubbingView: some View {
ZStack {
VStack {
Spacer()
scrubbingKnob
Spacer()
}
}
.contentShape(Rectangle())
}
private
var ctSliderRow: some View {
HStack {
Button {
rootHandler( .lighting(.increase(.colortemp,by:10.pt,on:light)) )
} label: {
Image(systemName:"thermometer.snowflake")
.foregroundStyle(ctColor(ct:(Double(light.ct) * 1.2)))
.accessibility(label:Text("increase color temperature"))
}.frame(width: 24)
Slider(value: $ct, in: (-500)...(-153)) { _ in rootHandler( .lighting(.apply(.values(.ct(Int(-ct),bri)),on:light))) }
.accessibility(label:Text("change color temperature"))
Button {
rootHandler( .lighting(.decrease(.colortemp,by:10.pt,on:light)) )
} label: {
Image(systemName:"thermometer.sun")
.foregroundStyle(ctColor(ct:(Double(light.ct) * 0.8)))
.accessibility(label:Text("decrease color temperature"))
}.frame(width: 24)
}
.rowStyle(height: (selectedMode == .ct && !isCollapsed) ? rowHeight : 0)
.opacity((selectedMode == .ct && !isCollapsed) ? 1.0 : 0.0)
}
private
var briSliderRow: some View {
slider(
bind: $bri,
leading:
.init(action: {
rootHandler( .lighting(.decrease(.brightness,by:10.pt,on:light)) ) },
image: .system("sun.min"),
color: hsbColor(brightness: min(max(light.brightness - 0.05, 0.1), 1.0)),
accessibility: "decrease brightness"),
slider:
.init(action: { rootHandler( .lighting(.apply(.values(.bri(bri)),on:light))) },
accessibility:"change brightness"),
trailing:
.init(action: {
rootHandler( .lighting(.increase(.brightness,by:10.pt,on:light)) ) },
image: .system("sun.max.fill"),
color: hsbColor(brightness: min(max(light.brightness + 0.05, 0.1), 1.0)),
accessibility: "increase brightness")
).rowStyle(height: rowHeight)
}
private
var hueSliderRow: some View {
slider(
bind: $hue,
leading:
.init(action: { rootHandler( .lighting(.decrease(.hue,by:10.pt,on:light)) ) },
image: .system("paintpalette"),
color: hsbColor(hue: min(max(light.hue - 0.05, 0.1), 1.0)),
accessibility: "decrease saturation"),
slider:
.init(action: { rootHandler( .lighting(.apply(.values(.hsb(hue, sat, bri)),on:light))) },
accessibility: "change saturation"),
trailing:
.init(
action: { rootHandler( .lighting(.increase(.hue,by:10.pt, on:light)) ) },
image: .system("paintpalette"),
color: hsbColor(hue: min(max(light.hue + 0.05, 0.1), 1.0)),
accessibility: "increase saturation")
)
.rowStyle(height: (selectedMode == .hsb && !isCollapsed) ? rowHeight : 0)
.opacity((selectedMode == .hsb && !isCollapsed) ? 1.0 : 0.0)
}
private
var satSliderRow: some View {
slider(
bind: $sat,
leading:
.init(action: { rootHandler( .lighting(.decrease(.saturation,by:10.pt, on:light)) ) },
image: .system("circle.lefthalf.fill"),
color: hsbColor(saturation: min(max(light.saturation - 0.25, 0.1), 1.0)),
accessibility: "decrease saturation"),
slider:
.init(action: { rootHandler( .lighting(.apply(.values( .hsb(hue, sat, bri)),on:light))) },
accessibility: "change saturation"),
trailing:
.init(
action: { rootHandler( .lighting(.increase(.saturation,by:10.pt, on:light)) ) },
image: .system("circle.righthalf.fill"),
color: hsbColor(saturation: min(max(light.saturation + 0.25, 0.1), 1.0)),
accessibility: "increase saturation")
)
.rowStyle(height: (selectedMode == .hsb && !isCollapsed) ? rowHeight : 0)
.opacity((selectedMode == .hsb && !isCollapsed) ? 1.0 : 0.0)
}
private
func imageForSlider(conf: SliderButtonConf.ButtonImage) -> Image { switch conf { case .system(let name): return Image(systemName: name) } }
private
func slider(
bind: Binding<Double>,
leading:SliderButtonConf, slider:SliderConf, trailing:SliderButtonConf) -> some View {
HStack {
Button {
leading.action()
} label: {
imageForSlider(conf: leading.image)
.foregroundStyle(leading.color)
.accessibility(label:Text(leading.accessibility))
}.frame(width: 24)
Slider(value: bind){ _ in
slider.action()
}.accessibility(label:Text(slider.accessibility))
Button {
trailing.action()
} label: {
imageForSlider(conf: trailing.image)
.foregroundStyle(trailing.color)
.accessibility(label:Text(trailing.accessibility))
}.frame(width: 24)
}
}
}
fileprivate
struct SliderButtonConf {
let action: () -> ()
let image: ButtonImage
let color: Color
let accessibility:String
enum ButtonImage {
case system(String)
}
}
fileprivate
struct SliderConf {
let action: () -> ()
let accessibility:String
}
private
extension LightCell {
func selected(_ mode: Light.Mode) { selectedMode = mode }
}
fileprivate
extension View {
#ViewBuilder func rowStyle(height:Double) -> some View {
self.frame(height:height)
.padding(.trailing, 15)
.padding(.leading, 15)
.background(material, in: RoundedRectangle(cornerRadius: 5))
}
#ViewBuilder func isCollapsed(_ isCollapsed:Bool) -> some View {
self.opacity(isCollapsed ? 0 : 1).frame(maxHeight: isCollapsed ? 0 : .infinity)
}
}
fileprivate
struct Height: AnimatableModifier {
init(_ height: Double) {
self.height = height
}
private var height = 0.0
var animatableData: Double {
get { height }
set { height = newValue }
}
func body(content: Content) -> some View {
content.frame(height: height)
}
}

Related

SwiftUI: Running into issues with Drag and Drop inside a LazyVGrid

I've been working on my own smart home app and have run into some issues when trying to build the grid for the app.
I've been basing this home app on this tutorial. The goal is that one can reorder the individually sized blocks in the grid basically like he or she wants. The blocks(items) represent different gadgets in the smart home application. The issue I'm facing is that I can't seem to get the drag & drop to work. Maybe it's better to put all the item views in one custom view and then run a "ForEach" loop for them so that the .onDrag works? I'm relatively new to SwiftUI so I appreciate every hint I can get this program to work.
Here is my code:
ItemModel1:
struct ItemModel: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}
ItemModel2:
struct ItemModel2: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel2 {
return ItemModel2(id: id, title: title)
}
}
It's essentially the same for the two other ItemModels 3 and 4..
ItemViewModel:
class ItemViewModel {
var items: [ItemModel] = []
#Published var currentGrid: ItemModel?
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "Item1"),
]
items.append(contentsOf: newItems)
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
ContentView:
struct DropViewDelegate: DropDelegate {
var grid: ItemModel
var gridData: ItemViewModel
func performDrop(info: DropInfo) -> Bool {
return true
}
func dropEntered(info: DropInfo) {
let fromIndex = gridData.items.firstIndex { (grid) -> Bool in
return self.grid.id == gridData.currentGrid?.id
} ?? 0
let toIndex = gridData.items.firstIndex { (grid) -> Bool in
return self.grid.id == self.grid.id
} ?? 0
if fromIndex != toIndex{
withAnimation(.default){
let fromGrid = gridData.items[fromIndex]
gridData.items[fromIndex] = gridData.items[toIndex]
gridData.items[toIndex] = fromGrid
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
}
struct ContentView: View {
#State var items: [ItemModel] = []
#State var items2: [ItemModel2] = []
#State var items3: [ItemModel3] = []
#State var items4: [ItemModel4] = []
#State var gridData = ItemViewModel()
let columns = [
GridItem(.adaptive(minimum: 160)),
GridItem(.adaptive(minimum: 160)),
]
let columns2 = [
GridItem(.flexible()),
]
var body: some View {
ZStack{
ScrollView{
VStack{
HStack(alignment: .top){
Button(action: saveButtonPressed, label: {
Text("Item1")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed2, label: {
Text("Item2")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed3, label: {
Text("Item3")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed4, label: {
Text("Item4")
.font(.title2)
.foregroundColor(.white)
})
}
LazyVGrid(
columns: columns,
alignment: .leading,
spacing: 12
){
ForEach(items) { item in
Item1View (item: item)
if 1 == 1 { Color.clear }
}
ForEach(items4) { item4 in
Item4View (item4: item4)
if 1 == 1 { Color.clear }
}
ForEach(items2) { item2 in
Item2View (item2: item2)
}
LazyVGrid(
columns: columns2,
alignment: .leading,
spacing: 12
){
ForEach(items3) { item3 in
Item3View (item3: item3)
}
}
}
.onDrag({
self.gridData = items
return NSItemProvider(item: nil, typeIdentifier:
self.grid)
})
.onDrop(of: [items], delegate: DropViewDelegate(grid:
items, gridData: gridData))
}
}
}
}
func saveButtonPressed() {
addItem(title: "Hello")
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func saveButtonPressed2() {
addItem2(title: "Hello")
}
func addItem2(title: String) {
let newItem = ItemModel2(title: title)
items2.append(newItem)
}
func saveButtonPressed3() {
addItem3(title: "Hello")
}
func addItem3(title: String) {
let newItem = ItemModel3(title: title)
items3.append(newItem)
}
func saveButtonPressed4() {
addItem4(title: "Hello")
}
func addItem4(title: String) {
let newItem = ItemModel4(title: title)
items4.append(newItem)
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
Item1:
struct Item1View: View {
#State var item: ItemModel
var body: some View {
HStack {
Text(item.title)
}
.padding( )
.frame(width: 396, height: 56)
.background(.black)
.cornerRadius(12.0)
}
}
Item2:
struct Item2View: View {
#State var item2: ItemModel2
var body: some View {
HStack {
Text(item2.title)
}
.padding( )
.frame(width: 182, height: 132)
.background(.black)
.cornerRadius(12.0)
}
}
Item3:
struct Item3View: View {
#State var item3: ItemModel3
var body: some View {
HStack {
Text(item3.title)
}
.padding( )
.frame(width: 182, height: 62)
.background(.black)
.cornerRadius(12.0)
}
}
Item4:
struct Item4View: View {
#State var item4: ItemModel4
var body: some View {
HStack {
Text(item4.title)
}
.padding( )
.frame(width: 396, height: 156)
.background(.black)
.cornerRadius(12.0)
}
}
I tried recreating the grid Asperi linked. However, the .onDrop doesn't seem to work like it should. The drop only occurs after you pressed another item to drag it. Only then will the previous items reorder themselves..
My version:
import SwiftUI
import UniformTypeIdentifiers
struct ItemModel6: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel6 {
return ItemModel6(id: id, title: title)
}
}
class Model: ObservableObject {
var data: [ItemModel6] = []
let columns = [
GridItem(.adaptive(minimum: 160)),
GridItem(.adaptive(minimum: 160)),
]
init() {
data = Array(repeating: ItemModel6(title: "title"), count:
100)
for i in 0..<data.count {
data[i] = ItemModel6(title: "Hello")
}
}
}
struct DemoDragRelocateView: View {
#StateObject private var model = Model()
#State private var dragging: ItemModel6?
var body: some View {
ScrollView {
LazyVGrid(columns: model.columns) {
ForEach(model.data) { item2 in GridItemView (item2:
item2)
.overlay(dragging?.id == item2.id ?
Color.white.opacity(0.8) : Color.clear)
.onDrag {
self.dragging = item2
return NSItemProvider(object:
String(item2.id) as NSString)
}
.onDrop(of: [UTType.text], delegate:
DragRelocateDelegate(item: item2, listData: $model.data,
current: $dragging))
}
}.animation(.default, value: model.data)
}
.onDrop(of: [UTType.text], delegate:
DropOutsideDelegate(current: $dragging))
}
}
struct DropOutsideDelegate: DropDelegate {
#Binding var current: ItemModel6?
func performDrop(info: DropInfo) -> Bool {
current = nil
return true
}
}
struct DragRelocateDelegate: DropDelegate {
let item: ItemModel6
#Binding var listData: [ItemModel6]
#Binding var current: ItemModel6?
func dropEntered(info: DropInfo) {
if item != current {
let from = listData.firstIndex(of: current!)!
let to = listData.firstIndex(of: item)!
if listData[to].id != current!.id {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
self.current = nil
return true
}
}
struct GridItemView: View {
#State var item2: ItemModel6
var body: some View {
HStack {
Text(item2.title)
}
.padding( )
.frame(width: 182, height: 132)
.background(.gray)
.cornerRadius(12.0)
}
}
struct DemoDragRelocateView_Previews: PreviewProvider {
static var previews: some View {
DemoDragRelocateView()
.preferredColorScheme(.dark)
}
}

KeyboardSceneDelegate: Animation styles were not empty on user driven presentation

I'm having problem with my app. When I tap on a TextField, trying to fill data, in the sheet that just show up, my app freezes and four errors appear on console:
> [Assert] Animation styles expected to be empty on user driven
> presentation. Actually contains: (
> "<<_UIViewControllerKeyboardAnimationStyle: 0x281ee4d00>; animated = YES; duration = 0.35; force = NO>" )
>
> [KeyboardSceneDelegate] Animation styles were not empty on user driven
> presentation!
>
> Fit_Vein/ProfileView.swift:69: Fatal error: Unexpectedly found nil
> while unwrapping an Optional value
>
> Fit_Vein/ProfileView.swift:69: Fatal error: Unexpectedly found nil
> while unwrapping an Optional value
This situation takes place when I go from ProfileView to SettingsView, then try to do one of options: change email, password or delete account and input data in those sheets.
My code:
ProfileView:
struct ProfileView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#EnvironmentObject private var sessionStore: SessionStore
#Environment(\.colorScheme) var colorScheme
#State private var image = UIImage()
#State private var shouldPresentAddActionSheet = false
#State private var shouldPresentImagePicker = false
#State private var shouldPresentCamera = false
#State private var shouldPresentSettings = false
init(profileViewModel: ProfileViewModel) {
self.profileViewModel = profileViewModel
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
ScrollView(.vertical) {
HStack {
if profileViewModel.profilePicturePhotoURL != nil {
AsyncImage(url: profileViewModel.profilePicturePhotoURL!) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
.shadow(color: .gray, radius: 7)
.frame(width: screenWidth * 0.4, height: screenHeight * 0.2)
.onTapGesture {
self.shouldPresentAddActionSheet = true
}
} placeholder: {
Image(uiImage: UIImage(named: "blank-profile-hi")!)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
.shadow(color: .gray, radius: 7)
.frame(width: screenWidth * 0.4, height: screenHeight * 0.2)
}
} else {
Image(uiImage: UIImage(named: "blank-profile-hi")!)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
.shadow(color: .gray, radius: 7)
.frame(width: screenWidth * 0.4, height: screenHeight * 0.2)
.onTapGesture {
self.shouldPresentAddActionSheet = true
}
}
Spacer(minLength: screenWidth * 0.05)
VStack {
HStack {
Text(profileViewModel.profile!.firstName)
.foregroundColor(.green)
.font(.system(size: screenHeight * 0.03))
.fontWeight(.bold)
Spacer()
NavigationLink(destination: SettingsView(profile: profileViewModel).environmentObject(sessionStore), isActive: $shouldPresentSettings) {
Button(action: {
shouldPresentSettings = true
}, label: {
Image(systemName: "slider.vertical.3")
.resizable()
.aspectRatio(contentMode: .fit)
})
.frame(width: screenWidth * 0.12, height: screenHeight * 0.04)
.foregroundColor(colorScheme == .dark ? .white : .black)
}
}
.padding(.top, screenHeight * 0.02)
HStack {
Text(profileViewModel.profile!.username)
.foregroundColor(Color(uiColor: UIColor.lightGray))
Spacer()
}
Spacer()
}
}
.padding()
VStack {
Text("Level 1")
.font(.system(size: screenHeight * 0.03))
.fontWeight(.bold)
RoundedRectangle(cornerRadius: 25)
.frame(width: screenWidth * 0.9)
.padding()
.overlay(
HStack {
RoundedRectangle(cornerRadius: 25)
.foregroundColor(.green)
.padding()
.frame(width: screenWidth * 0.7)
Spacer()
}
)
.shadow(color: .gray, radius: 7)
Text("7 / 10 Workouts")
}
Spacer()
}
.sheet(isPresented: $shouldPresentImagePicker) {
ImagePicker(sourceType: self.shouldPresentCamera ? .camera : .photoLibrary, selectedImage: self.$image)
.onDisappear {
profileViewModel.uploadPhoto(image: image)
}
}
.actionSheet(isPresented: $shouldPresentAddActionSheet) {
ActionSheet(title: Text("Add a new photo"), message: nil, buttons: [
.default(Text("Take a new photo"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = true
}),
.default(Text("Upload a new photo"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = false
}),
ActionSheet.Button.cancel()
])
}
}
}
}
SettingsView:
import SwiftUI
struct SettingsView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#StateObject private var sheetManager = SheetManager()
#State private var shouldPresentActionSheet = false
#Environment(\.dismiss) var dismiss
private class SheetManager: ObservableObject {
enum Sheet {
case email
case password
case logout
case signout
}
#Published var showSheet = false
#Published var whichSheet: Sheet? = nil
}
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
Form {
Section(header: Text("Chats")) {
Toggle(isOn: .constant(false), label: {
Text("Hide my activity status")
})
}
Section(header: Text("Account")) {
Button(action: {
sheetManager.whichSheet = .email
sheetManager.showSheet.toggle()
}, label: {
Text("Change e-mail address")
})
Button(action: {
sheetManager.whichSheet = .password
sheetManager.showSheet.toggle()
}, label: {
Text("Change password")
})
Button(action: {
sheetManager.whichSheet = .logout
shouldPresentActionSheet = true
}, label: {
Text("Logout")
.foregroundColor(.red)
})
Button(action: {
sheetManager.whichSheet = .signout
shouldPresentActionSheet = true
}, label: {
Text("Delete account")
.foregroundColor(.red)
})
}
Section(header: Text("Additional")) {
Label("Follow me on GitHub:", systemImage: "link")
.font(.system(size: 17, weight: .semibold))
Link("#Vader20FF", destination: URL(string: "https://github.com/Vader20FF")!)
.font(.system(size: 17, weight: .semibold))
}
}
.navigationBarTitle("Settings")
.navigationBarTitleDisplayMode(.large)
.sheet(isPresented: $sheetManager.showSheet) {
switch sheetManager.whichSheet {
case .email:
ChangeEmailAddressSheetView(profile: profileViewModel)
case .password:
ChangePasswordSheetView(profile: profileViewModel)
case .signout:
DeleteAccountSheetView(profile: profileViewModel)
default:
Text("No view")
}
}
.confirmationDialog(sheetManager.whichSheet == .logout ? "Are you sure you want to logout?" : "Are you sure you want to delete your account? All data will be lost.", isPresented: $shouldPresentActionSheet, titleVisibility: .visible) {
if sheetManager.whichSheet == .logout {
Button("Logout", role: .destructive) {
profileViewModel.sessionStore!.signOut()
dismiss()
}
Button("Cancel", role: .cancel) {}
} else {
Button("Delete Account", role: .destructive) {
sheetManager.showSheet.toggle()
}
Button("Cancel", role: .cancel) {}
}
}
}
}
}
struct DeleteAccountSheetView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#Environment(\.dismiss) var dismiss
#State private var email = ""
#State private var password = ""
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
NavigationView {
VStack {
Form {
Section(footer: Text("Before you delete your account please provide your login credentials to confirm it is really you.")) {
TextField("E-mail", text: $email)
SecureField("Password", text: $password)
}
}
Button(action: {
withAnimation {
dismiss()
profileViewModel.deleteUserData() {
profileViewModel.sessionStore!.deleteUser(email: email, password: password) {
print("Successfully deleted user.")
}
}
}
}, label: {
Text("Delete account permanently")
})
.frame(width: screenWidth * 0.7, height: screenHeight * 0.08)
.background(Color.green)
.cornerRadius(15.0)
.font(.system(size: screenHeight * 0.026))
.foregroundColor(.white)
.padding()
}
.navigationBarHidden(true)
.ignoresSafeArea(.keyboard)
}
}
}
}
struct ChangeEmailAddressSheetView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#Environment(\.dismiss) var dismiss
#State private var oldEmail = ""
#State private var password = ""
#State private var newEmail = ""
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
NavigationView {
VStack {
Form {
Section(footer: Text("Before you change your e-mail address please provide your login credentials to confirm it is really you.")) {
TextField("Old e-mail address", text: $oldEmail)
SecureField("Password", text: $password)
TextField("New e-mail address", text: $newEmail)
}
}
Button(action: {
withAnimation {
dismiss()
profileViewModel.emailAddressChange(oldEmailAddress: oldEmail, password: password, newEmailAddress: newEmail) {}
}
}, label: {
Text("Change e-mail address")
})
.frame(width: screenWidth * 0.7, height: screenHeight * 0.08)
.background(Color.green)
.cornerRadius(15.0)
.font(.system(size: screenHeight * 0.026))
.foregroundColor(.white)
.padding()
}
.navigationBarHidden(true)
.ignoresSafeArea(.keyboard)
}
}
}
}
struct ChangePasswordSheetView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#Environment(\.dismiss) var dismiss
#State private var email = ""
#State private var oldPassword = ""
#State private var newPassword = ""
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
NavigationView {
VStack {
Form {
Section(footer: Text("Before you change your password please provide your login credentials to confirm it is really you.")) {
TextField("E-mail", text: $email)
SecureField("Old password", text: $oldPassword)
SecureField("New password", text: $newPassword)
}
}
Button(action: {
withAnimation {
dismiss()
profileViewModel.passwordChange(emailAddress: email, oldPassword: oldPassword, newPassword: newPassword) {}
}
}, label: {
Text("Change password")
})
.frame(width: screenWidth * 0.7, height: screenHeight * 0.08)
.background(Color.green)
.cornerRadius(15.0)
.font(.system(size: screenHeight * 0.026))
.foregroundColor(.white)
.padding()
}
.navigationBarHidden(true)
.ignoresSafeArea(.keyboard)
}
}
}
}
ProfileViewModel:
import Foundation
import SwiftUI
#MainActor
class ProfileViewModel: ObservableObject {
#Published var sessionStore: SessionStore?
private let firestoreManager = FirestoreManager()
private let firebaseStorageManager = FirebaseStorageManager()
#Published var profile: Profile?
#Published var profilePicturePhotoURL: URL?
#Published var fetchingData = true
init(forPreviews: Bool) {
self.profile = Profile(id: "sessionStore!.currentUser!.uid", firstName: "firstname", username: "username", birthDate: Date(), age: 18, country: "country", city: "city", language: "language", gender: "gender", email: "email", profilePictureURL: nil)
}
init() {
Task {
try await fetchData()
}
}
func setup(sessionStore: SessionStore) {
self.sessionStore = sessionStore
}
func fetchData() async throws {
if sessionStore != nil {
if sessionStore!.currentUser != nil {
print("Fetching Data")
fetchingData = true
let (firstname, username, birthDate, age, country, city, language, gender, email, profilePictureURL) = try await self.firestoreManager.fetchDataForProfileViewModel(userID: sessionStore!.currentUser!.uid)
self.profile = Profile(id: sessionStore!.currentUser!.uid, firstName: firstname, username: username, birthDate: birthDate, age: age, country: country, city: city, language: language, gender: gender, email: email, profilePictureURL: profilePictureURL)
if profilePictureURL != nil {
self.firebaseStorageManager.getDownloadURLForImage(stringURL: profilePictureURL!, userID: sessionStore!.currentUser!.uid) { photoURL in
self.profilePicturePhotoURL = photoURL
}
}
Task {
fetchingData = false
}
}
} else {
fetchingData = false
}
}
func uploadPhoto(image: UIImage) {
if self.profile!.profilePictureURL != nil {
Task {
try await self.firebaseStorageManager.deleteImageFromStorage(userPhotoURL: self.profile!.profilePictureURL!, userID: self.profile!.id)
}
}
self.firebaseStorageManager.uploadImageToStorage(image: image, userID: self.profile!.id) { photoURL in
self.firestoreManager.addProfilePictureURLToUsersData(photoURL: photoURL) {
Task {
try await self.fetchData()
}
}
}
}
func emailAddressChange(oldEmailAddress: String, password: String, newEmailAddress: String, completion: #escaping (() -> ())) {
self.sessionStore!.changeEmailAddress(oldEmailAddress: oldEmailAddress, password: password, newEmailAddress: newEmailAddress) {
print("Successfully changed user's e-mail address")
}
}
func passwordChange(emailAddress: String, oldPassword: String, newPassword: String, completion: #escaping (() -> ())) {
self.sessionStore!.changePassword(emailAddress: emailAddress, oldPassword: oldPassword, newPassword: newPassword) {
print("Successfully changed user's password")
}
}
func deleteUserData(completion: #escaping (() -> ())) {
if self.profile!.profilePictureURL != nil {
self.firestoreManager.deleteUserData(userUID: sessionStore!.currentUser!.uid) {
print("Successfully deleted user data")
Task {
try await self.firebaseStorageManager.deleteImageFromStorage(userPhotoURL: self.profile!.profilePictureURL!, userID: self.sessionStore!.currentUser!.uid)
completion()
}
}
}
}
}
ProfileModel:
import Foundation
import SwiftUI
struct Profile: Codable, Identifiable {
var id: String
var firstName: String
var username: String
var birthDate: Date
var age: Int
var country: String
var city: String
var language: String
var gender: String
var email: String
var profilePictureURL: String?
}

ERROR: IOSurfaceCreate 'RGBA' failed when calling requestImage from PHCachingImageManager

I am using PHCachingImageManager to get thumbnail images for all images in the iOS photo library which are then rendered using a SwiftUI List. It seems to work for a small number of images (e.g. the six that load with the simulator), but given 1000s of images I'm incurring this error many times.
IIO_CreateIOSurfaceWithFormatAndBuffer:594: *** ERROR: IOSurfaceCreate 'RGBA' failed - clientAddress: 0x14d6a0000 allocSize: 0x00072000 size: 256 x 456 rb: 1024 [0x00000400] bpp: 4
What does this mean and what is the root cause? Does access to PHCachingImageManager need to be throttled down?
Below is a class similar to the one in my app that reproduces the issue on my iPhone SE2.
import Foundation
import SwiftUI
import UIKit
import Photos
let thumbnailSize = CGSize(width: 90, height: 90)
struct PhotoSelectView: View {
class ImageRowManager {
let thumbnailImageRequestOptions: PHImageRequestOptions
let cachingImageManager = PHCachingImageManager()
var rows: [SelectableImageRow] = []
init() {
let options = PHImageRequestOptions()
options.isSynchronous = true
options.resizeMode = .fast
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = false
self.thumbnailImageRequestOptions = options
}
func add(row: SelectableImageRow) {
self.rows.append(row)
}
}
struct SelectableImageRow: Hashable {
var rowIndex: Int
var images: [SelectableImage]
func hash(into hasher: inout Hasher) {
hasher.combine(self.rowIndex)
}
}
class SelectableImage: Hashable, ObservableObject {
#Published var image: UIImage? = nil
let id: String
private let asset: PHAsset
private let imageRowManager: ImageRowManager
init(asset: PHAsset, imageRowManager: ImageRowManager) {
self.id = asset.localIdentifier
self.asset = asset
self.imageRowManager = imageRowManager
self.loadImage()
}
func loadImage() {
DispatchQueue.global(qos: .background).async {
self.imageRowManager.cachingImageManager.requestImage(for: self.asset, targetSize: CGSize(width: 150, height: 150), contentMode: .aspectFill, options: self.imageRowManager.thumbnailImageRequestOptions) { (image, _) in
RunLoop.main.perform {
self.image = image
}
}
}
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
static func ==(lhs: SelectableImage, rhs: SelectableImage) -> Bool {
return lhs.id == rhs.id
}
}
let imageRowManager = ImageRowManager()
#State var selected = Set<SelectableImage>()
#State var grid: [SelectableImageRow] = []
var body: some View {
VStack {
VStack {
if !self.grid.isEmpty {
HStack {
Text("Pick images")
Spacer()
}
.padding(.leading)
.padding(.top)
ImagesScrollView(grid: self.$grid, selected: self.$selected)
Button(action: {
self.handleSelectButton()
}) {
Text("Select")
.foregroundColor(Color.black.opacity((self.selected.count != 0) ? 1 : 0.5))
.padding(.vertical,10)
.frame(width: UIScreen.main.bounds.width / 2)
.overlay(
Capsule(style: .continuous)
.stroke(Color.black.opacity((self.selected.count != 0) ? 1 : 0.5), style: StrokeStyle(lineWidth: 5))
)
}
.background(Color.white)
.padding(.bottom)
.disabled((self.selected.count != 0) ? false : true)
}
}
.frame(width: UIScreen.main.bounds.width - CGFloat(horizontalPadding), height: UIScreen.main.bounds.height / 1.5)
.background(Color.white)
.cornerRadius(12)
}
.background(Color.black.opacity(0.1)).edgesIgnoringSafeArea(.all)
.onAppear {
PHPhotoLibrary.requestAuthorization { status in
if status == .authorized {
self.getAllImages()
} else {
print("Cannot access photo library")
}
}
}
}
private func handleSelectButton() {
print("selected images", self.selected)
}
private func getAllImages() {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let req = PHAsset.fetchAssets(with: .image, options: fetchOptions)
var rowIndex = 0
for i in stride(from: 0, to: req.count, by: gridItemWidth) {
var iteration : [SelectableImage] = []
for j in i..<i+gridItemWidth {
if j < req.count{
iteration.append(SelectableImage(asset: req[j], imageRowManager: self.imageRowManager))
}
}
let row = SelectableImageRow(rowIndex: rowIndex, images: iteration)
imageRowManager.add(row: row)
rowIndex += 1
}
self.grid = imageRowManager.rows
}
// Subviews
struct ImagesScrollView: View {
#Binding var grid: [SelectableImageRow]
#Binding var selected: Set<SelectableImage>
var body: some View {
List(self.grid, id: \.self) { row in
SelectableImageRowView(row: row, selected: self.$selected)
}
}
}
struct SelectableImageRowView: View {
var row: SelectableImageRow
#Binding var selected: Set<SelectableImage>
var body: some View {
HStack(spacing: 2) {
ForEach(row.images, id: \.self) { img in
SelectableImageCard(data: img, selected: self.$selected)
}
}
}
}
struct SelectableImageCard: View {
#ObservedObject var data: SelectableImage
#Binding var selected: Set<SelectableImage>
var body: some View {
ZStack {
Image(uiImage: self.image()).resizable()
if self.selected.contains(self.data) {
Image(systemName: "checkmark")
.resizable()
.padding(7)
.foregroundColor(.white)
.background(Color.blue)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 1))
.frame(width: 30, height: 30, alignment: .topTrailing)
.offset(x: 30, y: -28)
}
}
.frame(width: thumbnailSize.width, height: thumbnailSize.height)
.onTapGesture {
if !self.selected.contains(self.data) {
self.selected.insert(self.data)
} else{
self.selected.remove(self.data)
}
}
}
private func image() -> some UIImage {
if let image = self.data.image {
return image
} else {
return UIImage(systemName: "heart.fill")!
}
}
}
}

SwiftUI => AttributeGraph: cycle detected through attribute after updating one model in list

Context:
I've got a list of custom views. The array is stored a #ObservableObject as #Published.
My custom view has a function which detects when the View is touched (I did it because it's triggered only after an animation). This event activates, through my #ObservableObject, an event that shows a View which is in ZStack with my list. There I could update my passed object through a TextField, and when I come back I have everything updated.
However, when I try to re-show one of every element in my list, my debug shows this error:
AttributeGraph: cycle detected through attribute.
Instead, if I show the detail without updating my model's data, I have not any leak.
Any suggestion?
EDIT:
here's the code:
struct ProcedureList: View {
#ObservedObject var procedureManager = ProcedureManager()
#State private var showModal = false
var isEmpty:Bool {
return procedureManager.procedures.isEmpty
}
init() {
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground()
UINavigationBar.appearance().scrollEdgeAppearance = appearance
UINavigationBar.appearance().standardAppearance = appearance
}
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
VStack{
if !self.isEmpty {
List {
ForEach(self.procedureManager.procedures.indices, id: \.self) { index in
ProcedureCell(procedure: self.$procedureManager.procedures[index]){ procedure, position, size in
self.procedureManager.selectedProcedure = procedure
self.procedureManager.cardSize = size
self.procedureManager.cardPosition = position
self.procedureManager.size = size
self.procedureManager.position = position
self.procedureManager.isPressed = true
withAnimation(Animation.default.delay(0.1)) {
self.procedureManager.size.width = geometry.frame(in: .local).width
self.procedureManager.size.height = geometry.frame(in: .local).size.height
self.procedureManager.position.x = geometry.frame(in: .global).origin.x
self.procedureManager.position.y = geometry.frame(in: .global).origin.y
}
print(
"""
pressed procedure: \(procedure.title)
at position: \(position)
and with size: \(size)
"""
)
}
// .tag(self.procedureManager.procedures[index])
.tag(index)
}
.onDelete(perform: self.onDelete)
}
.environment(\.defaultMinListRowHeight, 120)
.animation(.easeInOut)
}else {
VStack{
Text("Non hai ancora creato una procedura!")
.font(.largeTitle)
.multilineTextAlignment(.center)
.padding(.bottom, 30)
Button(action: {
self.showModal.toggle()
}){
Text("Creane una nuova!")
}
.sheet(isPresented: self.$showModal) {
NewProcedure(showModal: self.$showModal) { procedure in
self.procedureManager.newProcedure = procedure
self.procedureManager.createProcedure()
}
}
}.padding(20)
}
}
Rectangle()
.edgesIgnoringSafeArea(.all)
.zIndex(self.procedureManager.isPressed ? 0 : -1)
.opacity(self.procedureManager.isPressed ? 0.7 : 0)
.animation(Animation.easeInOut(duration: 0.5))
ProcedureDetail(action: { procedure in
self.procedureManager.update(procedure: procedure)
}, procedure: self.$procedureManager.selectedProcedure, isShowingDetail: self.$procedureManager.isPressed)
.frame(width: self.procedureManager.correctSize.width, height: self.procedureManager.correctSize.height)
.position(x: self.procedureManager.correctPosition.x, y: self.procedureManager.correctPosition.y - (geometry.frame(in: .global).origin.y))
.offset(x: self.procedureManager.correctSize.width / 2, y: self.procedureManager.correctSize.height / 2)
.animation(.easeInOut)
.opacity(self.procedureManager.correctOpacity)
.animation(Animation.easeInOut.delay(self.procedureManager.isPressed ? 0 : 0.2))
}
.onAppear {
UITableView.appearance().separatorStyle = .none
}
.onDisappear() {
UITableView.appearance().separatorStyle = .singleLine
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(trailing:
!self.isEmpty && !self.procedureManager.isPressed ?
Button(action: {
self.showModal.toggle()
}){
Image(systemName: "plus.circle.fill")
.font(Font.system(size: 40))
.foregroundColor(Color.red)
}
.sheet(isPresented: self.$showModal) {
NewProcedure(showModal: self.$showModal) { procedure in
self.procedureManager.newProcedure = procedure
self.procedureManager.createProcedure()
}
} : nil
)
}
}
}
private func onDelete(offsets: IndexSet) {
self.procedureManager.procedures.remove(atOffsets: offsets)
}
}
struct ProcedureCell: View {
#Binding var procedure: Procedure
#State var position:CGPoint = .zero
#State var size:CGSize = .zero
var action:(_ procedure:Procedure, _ position: CGPoint, _ size:CGSize)->Void
var body: some View {
return
GeometryReader { geometry in
Button(action: {
let position = geometry.frame(in: .global).origin
let size = geometry.frame(in: .global).size
self.action(self.procedure, position, size)
}){
HStack {
VStack(alignment: .leading) {
Text(self.procedure.title)
.font(.largeTitle)
Text(self.procedure.subtitle)
.font(.title)
}
.padding(10)
Spacer()
}
}
.buttonStyle(MyButtonStyle())
.padding([.top, .bottom])
.edgesIgnoringSafeArea(.all)
}
}
}
struct MyButtonStyle:ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.background(
Rectangle()
.fill(configuration.isPressed ? Color.red : Color.orange)
.cornerRadius(20)
.shadow(radius: configuration.isPressed ? 5 : 0)
)
.scaleEffect(configuration.isPressed ? 1.1 : 1)
.animation(.easeInOut)
}
}
struct Procedure: Identifiable {
var title: String
var subtitle: String
var id: String
static var empty:Procedure {
return Procedure(title: "", subtitle: "")
}
init (title:String, subtitle:String) {
self.id = UUID().uuidString
self.title = title
self.subtitle = subtitle
}
}
class ProcedureManager: ObservableObject {
#Published var procedures: [Procedure]
#Published var newProcedure = Procedure.empty
#Published var selectedProcedure = Procedure.empty
#Published var cardSize:CGSize = .zero
#Published var cardPosition:CGPoint = .zero
#Published var size:CGSize = .zero
#Published var position:CGPoint = .zero
#Published var isPressed:Bool = false
var correctSize:CGSize {
if isPressed {
return size
}
else{
return cardSize
}
}
var correctPosition:CGPoint {
if isPressed {
return position
}
else{
return cardPosition
}
}
var correctOpacity: Double {
return isPressed ? 1 : 0
}
func update(procedure:Procedure) {
if let index = procedures.compactMap({$0.id}).firstIndex(of: procedure.id) {
procedures[index].title = procedure.title
procedures[index].subtitle = procedure.subtitle
objectWillChange.send()
}
}
func createProcedure(){
procedures.append(newProcedure)
newProcedure = .empty
}
func createProcedure(with title:String, andSubtitle subtitle:String) {
let procedure = Procedure(title: title, subtitle: subtitle)
procedures.append(procedure)
}
init(){
procedures = [
Procedure(title: "test1", subtitle: "subtitletest1"),
Procedure(title: "test2", subtitle: "subtitletest2"),
Procedure(title: "test3", subtitle: "subtitletest3"),
Procedure(title: "test4", subtitle: "subtitletest4"),
Procedure(title: "test5", subtitle: "subtitletest5"),
]
}
}

How to make each element of List of String clickable?

I have the function below which provides me hashtags from a text.
Now, I am trying to make each of them clickable like this, but it does not work:
for element in msg.findHashtags(){
Button(action: {
print("go to the hashtags view")
}) {
Text(element).background(Color("bg"))
.foregroundColor(.white)
.clipShape(Capsule())
}
}
The answer should be like this:
ForEach( msg.findHashtags(), id: \.self ){element in
Button(action: {
print("go to the hashtags view")
}) {
Text(element).background(Color("bg"))
.foregroundColor(.white)
.clipShape(Capsule())
}
}
Here is an extension template for the autoWordwrapping:
struct PositionKey : PreferenceKey {
static var defaultValue : [[Int]] = []
static func reduce(value: inout [[Int]], nextValue: () -> [[Int]]) {
let next = nextValue()
if next.count == 2 { value += [next.first!]}
else { value.replaceSubrange(((value.count - 1) ..< value.count), with: [value.last! + next[0]]) }
}
typealias Value = [[Int]]
}
struct TextLink: View {
let array = ["tage1111111111ddfff11111","tag2","taffffg3","tafffg4","tag4", "taffffg3","tafffg4","tag4","tag4333ddffdd333333333","ta33333333","tag4333333333333",]
var body: some View {
var tempCurrentPosition : CGFloat = 0
var currentPosition : CGFloat = 0
return ZStack(alignment: .leading){
GeometryReader{ proxy in
HStack{
ForEach( self.array.indices , id: \.self ) { index in
TextTag(text: self.array[index]).fixedSize(horizontal: true, vertical: false).anchorPreference(key: PositionKey.self , value: .leading) { (value: Anchor<CGPoint>) in
if currentPosition == 0 { currentPosition = proxy[value].x}
if proxy.size.width > (proxy[value].x - currentPosition) { tempCurrentPosition = proxy[value].x
return [[index]]}
currentPosition = proxy[value].x
return [[index],[]]
}.transformAnchorPreference(key: PositionKey.self, value: .trailing) { ( currentValue, value: Anchor<CGPoint>) in
if currentValue.count == 1{
if proxy.size.width < (proxy[value].x - currentPosition) {
currentPosition = tempCurrentPosition
currentValue = [currentValue.first!, []]}
}
}
}
}.opacity(0.0).overlayPreferenceValue(PositionKey.self) { v -> AnyView in
return AnyView( VStack(alignment: .leading, spacing: 10){
ForEach(v , id: \.self){ row in
HStack{
ForEach(row , id: \.self){ item in
TextTag(text: self.array[item])
}
}
}
}.opacity(1.0)
)
}
}
}
}
}
struct TextTag: View {
var text: String
var body: some View {
Button(action:{print(self.text)}){
Text(text).padding().background(Color.blue)
.foregroundColor(.white)
.clipShape(Capsule())
}}
}

Resources