SwiftUI For Each Button in list - not working... sometimes - swift

I have made a custom selection button view in SwiftUI for an app that is being developed. I cant for the life of me work out why sometimes the buttons don't do anything - It is always the last x number of buttons that don't work (which made me think it was related to the 10 view limitation of swift ui however, I've been told this isn't an issue when using a for each loop).
Sometimes it works as expected and others it cuts off the last x number of buttons. Although when it is cutting off buttons it is consistent between different simulators and physical devices. Can anybody see anything wrong here?
I am new to SwiftUI and so could be something simple...
#EnvironmentObject var QuestionManager: questionManager
var listItems: [String]
#State var selectedItem: String = ""
var body: some View {
GeometryReader {geom in
ScrollView{
VStack{
ForEach(Array(listItems.enumerated()), id: \.offset){ item in
Button(action: {
if (selectedItem != item.element) {
selectedItem = item.element
} else {
selectedItem = ""
QuestionManager.tmpAnswer = ""
}
}, label: {
GeometryReader { g in
Text("\(item.element)")
.font(.system(size: g.size.width/22))
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.black)
.lineLimit(2)
.frame(width: g.size.width, height: g.size.height)
.minimumScaleFactor(0.5)
.background(
Rectangle()
.fill((item.element == selectedItem) ? Color(.green) : .white)
.frame(width: g.size.width, height: g.size.height)
.border(Color.gray)
).scaledToFit()
}
.frame(width: geom.size.width*0.92, height: 45)
}).disabled((Int(QuestionManager.answers.year) == Calendar.current.component(.year, from: Date())) ? validateMonth(month: item.offset) : false)
}
}
.frame(width: geom.size.width)
}
}
}
} ```

As #Yrb mentioned, using enumerated() is not a great option in a ForEach loop.
Your issue could be compounded by listItems having duplicate elements.
You may want to restructure your code, something like this approach using a dedicated
item structure, works very well in my tests:
struct MyItem: Identifiable, Equatable {
let id = UUID()
var name = ""
init(_ str: String) {
self.name = str
}
static func == (lhs: MyItem, rhs: MyItem) -> Bool {
lhs.id == rhs.id
}
}
struct ContentView: View {
#EnvironmentObject var QuestionManager: questionManager
// for testing
var listItems: [MyItem] = [MyItem("1"),MyItem("2"),MyItem("3"),MyItem("4"),MyItem("6"),MyItem("7"),MyItem("8"),MyItem("9")]
#State var selectedItem: MyItem? = nil
var body: some View {
GeometryReader {geom in
ScrollView{
VStack{
ForEach(listItems){ item in
Button(action: {
if (selectedItem != item) {
selectedItem = item
} else {
selectedItem = nil
QuestionManager.tmpAnswer = ""
}
}, label: {
GeometryReader { g in
Text(item.name)
.font(.system(size: g.size.width/22))
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.black)
.lineLimit(2)
.frame(width: g.size.width, height: g.size.height)
.minimumScaleFactor(0.5)
.background(
Rectangle()
.fill((item == selectedItem) ? Color(.green) : .white)
.frame(width: g.size.width, height: g.size.height)
.border(Color.gray)
).scaledToFit()
}
.frame(width: geom.size.width*0.92, height: 45)
})
.disabled((Int(QuestionManager.answers.year) == Calendar.current.component(.year, from: Date())) ? validateMonth(item: item) : false)
}
}
.frame(width: geom.size.width)
}
}
}
func validateMonth(item: MyItem) -> Bool {
if let itemOffset = listItems.firstIndex(where: {$0.id == item.id}) {
// ... do your validation
return true
}
return false
}
}

Related

SwiftUI View method called but output missing

I have the View AlphabetLetterDetail:
import SwiftUI
struct AlphabetLetterDetail: View {
var alphabetLetter: AlphabetLetter
var letterAnimView : LetterAnimationView
var body: some View {
VStack {
Button(action:
animateLetter
) {
Image(uiImage: UIImage(named: "alpha_be_1")!)
.resizable()
.scaledToFit()
.frame(width: 60.0, height: 120.0)
}
letterAnimView
}.navigationBarTitle(Text(verbatim: alphabetLetter.name), displayMode: .inline)
}
func animateLetter(){
print("tapped")
letterAnimView.timerWrite()
}
}
containing the View letterAnimView of Type LetterAnimationView:
import SwiftUI
struct LetterAnimationView: View {
#State var Robot : String = ""
let LETTER =
["alpha_be_1_81",
"alpha_be_1_82",
"alpha_be_1_83",
"alpha_be_1_84",
"alpha_be_1_85",
"alpha_be_1_86",
"alpha_be_1_87",
"alpha_be_1_88",
"alpha_be_1_89",
"alpha_be_1_90",
"alpha_be_1"]
var body: some View {
VStack(alignment:.center){
Image(Robot)
.resizable()
.frame(width: 80, height: 160, alignment: .center)
.onAppear(perform: timerWrite)
}
}
func timerWrite(){
var index = 0
let _ = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) {(Timer) in
Robot = LETTER[index]
print("one frame")
index += 1
if (index > LETTER.count - 1){
Timer.invalidate()
}
}
}
}
This gives me a fine animation, as coded in func timerWrite() and performed by .onAppear(perform: timerWrite).
After commenting //.onAppear(perform: timerWrite) I try animating by clicking
Button(action: animateLetter)
but nothing happens.
Maybe I got two different instances of letterAnimView, if so why?
Can anybody of you competent guys intentify my mistake?
Regards - Klaus
You don't want to store Views, they are structs so they are copied. Instead, create an ObservableObject to encapsulate this functionality.
I created RobotModel here with other minor changes:
class RobotModel: ObservableObject {
private static let atlas = [
"alpha_be_1_81",
"alpha_be_1_82",
"alpha_be_1_83",
"alpha_be_1_84",
"alpha_be_1_85",
"alpha_be_1_86",
"alpha_be_1_87",
"alpha_be_1_88",
"alpha_be_1_89",
"alpha_be_1_90",
"alpha_be_1"
]
#Published private(set) var imageName: String
init() {
imageName = Self.atlas.last!
}
func timerWrite() {
var index = 0
let _ = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] timer in
guard let self = self else { return }
self.imageName = Self.atlas[index]
print("one frame")
index += 1
if index > Self.atlas.count - 1 {
timer.invalidate()
}
}
}
}
struct AlphabetLetterDetail: View {
#StateObject private var robot = RobotModel()
let alphabetLetter: AlphabetLetter
var body: some View {
VStack {
Button(action: animateLetter) {
Image(uiImage: UIImage(named: "alpha_be_1")!)
.resizable()
.scaledToFit()
.frame(width: 60.0, height: 120.0)
}
LetterAnimationView(robot: robot)
}.navigationBarTitle(Text(verbatim: alphabetLetter.name), displayMode: .inline)
}
func animateLetter() {
print("tapped")
robot.timerWrite()
}
}
struct LetterAnimationView: View {
#ObservedObject var robot: RobotModel
var body: some View {
VStack(alignment:.center){
Image(robot.imageName)
.resizable()
.frame(width: 80, height: 160, alignment: .center)
.onAppear(perform: robot.timerWrite)
}
}
}

AnyTransition issue with simple view update in macOS

I have 2 user view called user1 and user2, I am updating user with button, and I want give a transition animation to update, but for some reason my transition does not work, as I wanted, the issue is there that Text animated correctly but image does not, it stay in its place and it does not move with Text to give a smooth transition animation.
struct ContentView: View {
#State var show: Bool = Bool()
var body: some View {
VStack {
if (show) {
UserView(label: { Text("User 1") })
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: Edge.trailing), removal: AnyTransition.move(edge: Edge.leading)))
}
else {
UserView(label: { Text("User 2") })
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: Edge.leading), removal: AnyTransition.move(edge: Edge.trailing)))
}
Button("update") { show.toggle() }
}
.padding()
.animation(Animation.linear(duration: 1.0), value: show)
}
}
struct UserView<Label: View>: View {
let label: () -> Label
#State private var heightOfLabel: CGFloat? = nil
var body: some View {
HStack {
if let unwrappedHeight: CGFloat = heightOfLabel {
Image(systemName: "person")
.resizable()
.frame(width: unwrappedHeight, height: unwrappedHeight)
}
label()
.background(GeometryReader { proxy in
Color.clear
.onAppear(perform: { heightOfLabel = proxy.size.height })
})
Spacer(minLength: CGFloat.zero)
}
.animation(nil, value: heightOfLabel)
}
}
the heightOfLabel doesn't have to be optional, and then it works:
struct UserView<Label: View>: View {
let label: () -> Label
#State private var heightOfLabel: CGFloat = .zero // not optional
var body: some View {
HStack {
Image(systemName: "person")
.resizable()
.frame(width: heightOfLabel, height: heightOfLabel)
label()
.background(GeometryReader { proxy in
Color.clear
.onAppear(perform: { heightOfLabel = proxy.size.height })
})
Spacer(minLength: CGFloat.zero)
}
.animation(nil, value: heightOfLabel)
}
}

SwiftUI .sheet issues

I have been working on an app recently, and in the App, I use an ImagePicker in order to post images. I have my TabBarView below, and I have a button that should present a sheet with the ImagePicker, but the sheet is not being presented. Here, I just replaced the image picker with some text in order to ensure the issue isn't with the ImagePicker. I know the problem is probably trivial, but I can't seem to figure it out. Any help would be appreciated!
I took out a bunch of the TabBarView out here, so if you need more of the code, just let me know.
struct TabBarView: View {
#State var pickAnImage: Bool = false
#State var showImagePicker: Bool = false
#State var showCameraPicker: Bool = false
#State var image: UIImage?
#ObservedObject var viewRouter = ViewRouter()
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0){
Spacer()
if self.viewRouter.currentView == "home" {
HomeView2(dataHandler: dataHandler)
} else if self.viewRouter.currentView == "profile" {
ProfileView()
}
else if self.viewRouter.currentView == "new-entry" {
NewEntryView(image: self.image)
}else if self.viewRouter.currentView == "explore" {
ExploreView()
}else if self.viewRouter.currentView == "settings" {
SettingsView()
}
Spacer()
ZStack {
Circle()
.foregroundColor(yellow)
.frame(width: 70, height: 70)
Image(systemName: "photo")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(20)
.frame(width: 70, height: 70)
.foregroundColor(.white)
}.onTapGesture {
self.viewRouter.currentView = "home"
self.showImagePicker.toggle()
self.pickAnImage.toggle()
self.showPopUp = false
}
}
}
}
.sheet(isPresented: self.$pickAnImage, content: {
VStack{
Text("Hello")
Text("World!!")
}
})
}
}
}

SwiftUI automatically sizing bottom sheet

There are a lot of examples of bottom sheet out there for SwiftUI, however they all specify some type of maximum height the sheet can grow to using a GeometryReader. What I would like is to create a bottom sheet that becomes only as tall as the content within it. I've come up with the solution below using preference keys, but there must be a better solution. Perhaps using some type of dynamic scrollView is the solution?
struct ContentView: View{
#State private var offset: CGFloat = 0
#State private var size: CGSize = .zero
var body: some View{
ZStack(alignment:.bottom){
VStack{
Button(offset == 0 ? "Hide" : "Show"){
withAnimation(.linear(duration: 0.2)){
if offset == 0{
offset = size.height
} else {
offset = 0
}
}
}
.animation(nil)
.padding()
.font(.largeTitle)
Spacer()
}
BottomView(offset: $offset, size: $size)
}.edgesIgnoringSafeArea(.all)
}
}
struct BottomView: View{
#Binding var offset: CGFloat
#Binding var size: CGSize
var body: some View{
VStack(spacing: 0){
ForEach(0..<5){ value in
Rectangle()
.fill(value.isMultiple(of: 2) ? Color.blue : Color.red)
.frame(height: 100)
}
}
.offset(x: 0, y: offset)
.getSize{
size = $0
offset = $0.height
}
}
}
struct SizePreferenceKey: PreferenceKey {
struct SizePreferenceData {
let bounds: Anchor<CGRect>
}
static var defaultValue: [SizePreferenceData] = []
static func reduce(value: inout [SizePreferenceData], nextValue: () -> [SizePreferenceData]) {
value.append(contentsOf: nextValue())
}
}
struct SizePreferenceModifier: ViewModifier {
let onAppear: (CGSize)->Void
func body(content: Content) -> some View {
content
.anchorPreference(key: SizePreferenceKey.self, value: .bounds, transform: { [SizePreferenceKey.SizePreferenceData( bounds: $0)] })
.backgroundPreferenceValue(SizePreferenceKey.self) { preferences in
GeometryReader { geo in
Color.clear
.onAppear{
let size = CGSize(width: geo.size.width, height: geo.size.height)
onAppear(size)
}
}
}
}
}
extension View{
func getSize(_ onAppear: #escaping (CGSize)->Void) -> some View {
return self.modifier(SizePreferenceModifier(onAppear: onAppear))
}
}
Talk about over engineering the problem. All you have to do is specify a height of 0 if you want the sheet to be hidden, and not specify a height when it's shown. Additionally set the frame alignment to be top.
struct ContentView: View{
#State private var hide = false
var body: some View{
ZStack(alignment: .bottom){
Color.blue
.overlay(
Text("Is hidden : \(hide.description)").foregroundColor(.white)
.padding(.bottom, 200)
)
.onTapGesture{
hide.toggle()
}
VStack(spacing: 0){
ForEach(0..<5){ index in
Rectangle()
.foregroundColor(index.isMultiple(of: 2) ? Color.gray : .orange)
.frame(height: 50)
.layoutPriority(2)
}
}
.layoutPriority(1)
.frame(height: hide ? 0 : nil, alignment: .top)
.animation(.linear(duration: 0.2))
}.edgesIgnoringSafeArea(.all)
}
}
My approach is SwiftUI Sheet based solution feel free to check the gist
you just need to add the modifier to the view and let iOS do the rest for you, no need to re-do the math ;)
Plus you will have the sheet native behavior (swipe to dismiss) and i added "tap elsewhere" to dismiss.
struct ContentView: View {
#State var activeSheet: Bool = false
#State var activeBottomSheet: Bool = false
var body: some View {
VStack(spacing: 16){
Button {
activeSheet.toggle()
} label: {
HStack {
Text("Activate Normal sheet")
.padding()
}.background(
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 2)
.foregroundColor(.yellow)
)
}
Button {
activeBottomSheet.toggle()
} label: {
HStack {
Text("Activate Bottom sheet")
.padding()
}.background(
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 2)
.foregroundColor(.yellow)
)
}
}
.sheet(isPresented: $activeSheet) {
// Regular sheet
sheetView
}
.sheet(isPresented: $activeBottomSheet) {
// Responsive sheet
sheetView
.asResponsiveSheet()
}
}
var sheetView: some View {
VStack(spacing: 0){
ForEach(0..<5){ index in
Rectangle()
.foregroundColor(index.isMultiple(of: 2) ? Color.gray : .orange)
.frame(height: 50)
}
}
}
iPhone:
iPad :

My view moves up when I implemented the navigation link in swiftui

Mockup of the Application
Problem:
My application successfully navigates from one view to another without any complexities.When I use the navigationLink to navigate from View 4 to View 2 (refer mockup). The view 2 movesup. I tried debugging but I found no solution.
I have designed a mockup of what I am trying to acheive.
Code Block for View 4:
import SwiftUI
import BLE
struct View4: View {
#EnvironmentObject var BLE: BLE
#State private var showUnpairAlert: Bool = false
#State private var hasConnected: Bool = false
#State private var activateLink: Bool = false
let defaults = UserDefaults.standard
let defaultDeviceinformation = "01FFFFFFFFFF"
struct Keys {
static let deviceInformation = "deviceInformation"
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
NavigationLink(destination: View2(), isActive: $activateLink,label: { EmptyView() })
// MARK: - Menu Bar
HStack(alignment: .center, spacing: 10) {
VStack(alignment: .center, spacing: 4) {
Text(self.hasConnected ? "PodId \(checkForDeviceInformation())":"Pod is not connected")
.font(.footnote)
.foregroundColor(.white)
Button(action: {
print("Unpair tapped!")
self.showUnpairAlert = true
}) {
HStack {
Text("Unpair")
.fontWeight(.bold)
.font(.body)
}
.frame(minWidth: 85, minHeight: 35)
.foregroundColor(.white)
.background(Color(red: 0.8784313725490196, green: 0.34509803921568627, blue: 0.36470588235294116))
.cornerRadius(30)
}
}
}
}
.alert(isPresented: $showUnpairAlert) {
Alert(title: Text("Unpair from \(checkForDeviceInformation())"), message: Text("Do you want to unpair the current pod?"), primaryButton: .destructive(Text("Unpair")) {
self.unpairAndSetDefaultDeviceInformation()
}, secondaryButton: .cancel())
}
}
func checkForDeviceInformation() -> String {
let deviceInformation = defaults.value(forKey: Keys.deviceInformation) as? String ?? ""
print("Device Info \(deviceInformation)")
return deviceInformation
}
func unpairAndSetDefaultDeviceInformation() {
defaults.set(defaultDeviceinformation, forKey: Keys.deviceInformation)
print("Pod unpaired and view changed to Onboarding")
DispatchQueue.main.async {
self.activateLink = true
}
}
}
Thank you !!!!