SwiftUI removing item from Form causes crash [duplicate] - swift

As you can see from the image I have a list of colors, I would like to be able to also give the possibility to delete a color from the array.
I tried to add a list and then call the onDelete call on ForEach, but it's not working well it gives me problems.
Then in addition to this I would like the list to be the size of the contained elements.
Error:
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
Can anyone give me some advice?
Code:
import SwiftUI
struct ContentView: View {
var cornerRadius: CGFloat = 16
#State public var select = 2
#State public var bgColors: [Color] =
[
Color(red: 21.0/255.0, green: 101.0/255.0, blue: 192.0/255.0),
Color(red: 255.0/255.0, green: 193.0/255.0, blue: 7.0/255.0),
Color(red: 76.0/255.0, green: 175.0/255.0, blue: 80.0/255.0)
]
#Environment(\.colorScheme) var colorScheme
#State var isShowPicker: Bool = false
#State var image: Image? = Image("placeholder")
#State private var url: String = "https://a.wattpad.com/useravatar/climaxmite.256.718018.jpg"
init() {
// Segmented control colors
UISegmentedControl.appearance().backgroundColor = .systemGray6
UISegmentedControl.appearance().selectedSegmentTintColor = UIColor(Color.blue)
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.systemBackground], for: .selected)
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.label], for: .normal)
}
var body: some View {
VStack{
ZStack {
RoundedRectangle(cornerRadius: cornerRadius)
.frame(width: UIScreen.main.bounds.width-40, height: 100, alignment: .center)
.foregroundColor(colorScheme == .dark ? .black : .white)
VStack(spacing: 12) {
ZStack {
Rectangle()
.frame(width: UIScreen.main.bounds.width-47, height: 35, alignment: .center)
.foregroundColor(Color(UIColor.systemGray6))
.cornerRadius(cornerRadius, corners: [.topLeft, .topRight])
Text("Select Background")
.foregroundColor(Color(UIColor.label))
.font(.subheadline)
.bold()
}
Picker(selection: $select, label: Text("Select Background")) {
Text("Url").tag(0)
Text("Select Image").tag(1)
Text("Gradient").tag(2)
}.pickerStyle(SegmentedPickerStyle())
.padding(EdgeInsets(top: 0, leading: 30, bottom: 0, trailing: 30))
Spacer()
.frame(height: 3)
}
}
if self.select == 0 {
VStack{
ZStack {
RoundedRectangle(cornerRadius: cornerRadius)
.frame(width: UIScreen.main.bounds.width-40, height: 42, alignment: .center)
.foregroundColor(Color(UIColor.systemBackground))
TextField("http://", text: $url)
.padding(10)
.frame(width: UIScreen.main.bounds.width-40)
.foregroundColor(Color(UIColor.label))
.cornerRadius(cornerRadius)
.padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 10))
}
Button(action: {
}, label: {
Text("Submit")
.foregroundColor(Color(UIColor.systemBackground))
.bold()
})
.padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20))
.foregroundColor(.white)
.font(.subheadline)
.background(Color.blue)
.cornerRadius(cornerRadius)
}
}
if self.select == 1 {
VStack {
Button(action: {
withAnimation {
self.isShowPicker.toggle()
}
}) {
Image(systemName: "photo")
.font(.headline)
.foregroundColor(colorScheme == .dark ? .white : .black)
Text("Import")
.font(.headline)
.foregroundColor(colorScheme == .dark ? .white : .black)
}
.foregroundColor(.black)
}
.sheet(isPresented: $isShowPicker) {
ImagePicker(image: self.$image)
}
}
if self.select == 2 {
VStack(alignment: .trailing){
Button(action: {
bgColors.append(Color.clear)
}) {
Image(systemName: "plus")
.font(.headline)
.foregroundColor(colorScheme == .dark ? .white : .black)
.padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 15))
}
List {
ForEach(Array(bgColors.enumerated()), id: \.offset) { index, element in
ZStack {
ColorPicker("Set the background color", selection: $bgColors[index])
}
.padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 10))
} .onDelete(perform: delete)
}.background(Color.blue)
}
}
Spacer()
}
.padding(.top, 25)
.ignoresSafeArea(.keyboard)
.background(Color(UIColor.systemGray6))
.edgesIgnoringSafeArea(.all)
}
func delete(at offsets: IndexSet) {
bgColors.remove(atOffsets: offsets)
}
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape( RoundedCorner(radius: radius, corners: corners) )
}
}
// extension for keyboard to dismiss
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
struct ImagePicker: UIViewControllerRepresentable {
#Environment(\.presentationMode)
var presentationMode
#Binding var image: Image?
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#Binding var presentationMode: PresentationMode
#Binding var image: Image?
init(presentationMode: Binding<PresentationMode>, image: Binding<Image?>) {
_presentationMode = presentationMode
_image = image
}
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
image = Image(uiImage: uiImage)
presentationMode.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
presentationMode.dismiss()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentationMode: presentationMode, image: $image)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}

The problem is that in your List, the id you give it is \.offset. However, since you are removing data from bgColors, so this data can change. Instead, you should set the id as \.element because it will be constant for each color.
Consider this simplified example, which crashes when you remove a Color from the list:
struct ContentView: View {
#State private var arr: [Color] = [.red, .green, .blue]
var body: some View {
List {
ForEach(Array(arr.enumerated()), id: \.offset) { (index, _) in
ColorPicker("Color", selection: $arr[index])
}
.onDelete(perform: delete)
}
}
private func delete(at offsets: IndexSet) {
arr.remove(atOffsets: offsets)
}
}
And the working example, where the changes are the id given to the List, and the new Binding to the color (note the custom Binding for the selection):
struct ContentView: View {
#State private var arr: [Color] = [.red, .green, .blue]
var body: some View {
List {
ForEach(Array(arr.enumerated()), id: \.element) { (index, _) in
ColorPicker(
"Color",
selection: Binding<Color>(
get: { arr[index] },
set: { arr[index] = $0 }
)
)
}
.onDelete(perform: delete)
}
}
private func delete(at offsets: IndexSet) {
arr.remove(atOffsets: offsets)
}
}

Related

#State Property not accumulating

Users can swipe left or right on a stack of cards to simulate "true" or "false" to a series of quiz questions. I have a #State var called userScore initialized to 0. When dragGesture .onEnded, I compare the "correctAnswer" with the "userAnswer." If they match, add 1 point to the userScore.
The problem: The console prints 1 or 0. The score does not ACCUMULATE.
Please help me calculate the user's final score? Thanks in advance. . .
import SwiftUI
struct CardView: View {
#State var offset: CGSize = .zero
#State var userScore: Int = 0
#State var userAnswer: Bool = false
private var currentQuestion: Question
private var onRemove: (_ user: Question) -> Void
init(user: Question, onRemove: #escaping (_ user: Question) -> Void) {
self.currentQuestion = user
self.onRemove = onRemove
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25, style: .continuous)
.fill(
Color.black
.opacity(1 - Double(abs(offset.width / 50)))
)
.background(
RoundedRectangle(cornerRadius: 25, style: .continuous)
.fill(offset.width > 0 ? Color.green : Color.red)
//.overlay(offset.width > 0 ? likeGraphics() : dislikeGraphics(), alignment: .topLeading)
)
.shadow(color: .red, radius: 5, x: 0.0, y: 0.0)
VStack {
Image(currentQuestion.imageIcon)
.resizable()
.scaledToFit()
.foregroundColor(Color.white)
.frame(width: 75, height: 75)
ScrollView {
Text(currentQuestion.questionText)
.font(.largeTitle)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
}
}
.padding()
}
.frame(width: 300, height: 375) //check: www.ios-resolution.com
.offset(offset)
.rotationEffect(.degrees(offset.width / 400.0 ) * 15, anchor: .bottom)
//.opacity(2 - Double(abs(offset.width / 50.0))) //fade on drag too?
.gesture(
DragGesture()
.onChanged { gesture in
withAnimation(.spring()) {
offset = gesture.translation
}
}
.onEnded { value in
withAnimation(.interactiveSpring()) {
if abs(offset.width) > 100 {
onRemove(currentQuestion)
if (currentQuestion.correctAnswer == determineSwipeStatus()) {
userScore += 1
}
print(userScore)
} else {
offset = .zero
}
}
}
)
}
func determineSwipeStatus() -> Bool {
if(offset.width > 0) {
userAnswer = true
} else if (offset.width < 0) {
userAnswer = false
}
return userAnswer
}
func getAlert() -> Alert {
return Alert(
title: Text("Your group is \(userScore)% cult-like!"),
message: Text("Would you like to test another group?"),
primaryButton: .default(Text("Yes, test another group"),
action: {
//transition back to screen one
//self.showQuizPageScreen.toggle()
}),
secondaryButton: .cancel())
}
}
struct CardView_Previews: PreviewProvider {
static var previews: some View {
CardView(user: Question(id: 1,
imageIcon: "icon1",
questionText: "Dummy placeholder text",
correctAnswer: true),
onRemove: { _ in
//leave blank for preview purposes
})
}
}

How to to add Blur overlay on top of all views using .Blur as ViewModifier in SwiftUI

I have created a viewModifier which blurs a view when added.
Problem is when I add it on parent view of all the contents, all views are blurred differently.
I assume it's because it goes to all the contents and blur each of them individually instead of adding an overlay blur to the parent view.
I used Swift UI's native blur because UIBlurEffect has very limited configuration i.e. I can't adjust the blur intensity to a specific value.
Here it is when the modifier is added in the parent ZStack:
Here when I add it on background image only:
Adding it to background image looks good but I need it to be on top of all views. Here is my code:
Main View
import SwiftUI
struct BlurViewDemo: View {
#State private var isPressed = true
#State private var blurColor: BlurColor = .none
var body: some View {
ZStack {
GeometryReader { proxy in
let frame = proxy.frame(in: .global)
Image("blur-view-demo-image")
.resizable()
.frame(width: frame.size.width, height: frame.size.height)
// When added here only the background image is blurred.
.modifier(
BlurModifier(
showBlur: $isPressed, blurColor: $blurColor
)
)
}
VStack(spacing: 30) {
// TITLE STYLE
Text(getSelectedBlurTitle())
.font(.system(size: 60))
.offset(y: -40)
.foregroundColor(
blurColor == .dark && isPressed ? .white : .black
)
// TOGGLE BLUR BUTTON
Button(action: {
isPressed.toggle()
}, label: {
Text(isPressed ? "Blur: On" : "Blur: Off")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color(#colorLiteral(red: 0.2, green: 0.4, blue: 0.4, alpha: 1)))
// DEFAULT BUTTON
Button(action: {
isPressed = true
blurColor = .none
}, label: {
Text("Default")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color.gray)
// LIGHT BUTTON
Button(action: {
isPressed = true
blurColor = .light
}, label: {
Text("Light")
.foregroundColor(.black)
})
.frame(width: 80, height: 40)
.background(Color.white)
// DARK BUTTON
Button(action: {
isPressed = true
blurColor = .dark
}, label: {
Text("Dark")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color.black)
} //: VSTACK
} //: ZSTACK
.edgesIgnoringSafeArea(.all)
// When added here, the buttons are not blurred properly.
.modifier(
BlurModifier(
showBlur: $isPressed, blurColor: $blurColor
)
)
}
private func getSelectedBlurTitle() -> String {
guard isPressed else { return "Clear"}
switch blurColor {
case .none:
return "Default"
case .light:
return "Light"
case .dark:
return "Dark"
}
}
}
struct BlurViewDemo_Previews: PreviewProvider {
static var previews: some View {
BlurViewDemo()
}
}
View Modifier
public enum BlurColor {
case none
case light
case dark
}
import SwiftUI
struct BlurModifier: ViewModifier {
#Binding private var showBlur: Bool
#Binding private var blurColor: BlurColor
#State private var blurRadius: CGFloat = 14
public init(showBlur: Binding<Bool>, blurColor: Binding<BlurColor>) {
self._showBlur = showBlur
self._blurColor = blurColor
}
func body(content: Content) -> some View {
ZStack {
content
.blur(radius: showBlur ? blurRadius : 0, opaque: true)
.animation(Animation.easeInOut(duration: 0.3))
.edgesIgnoringSafeArea(.all)
.navigationBarHidden(showBlur ? true : false)
Rectangle()
.fill(showBlur ? getBlurColor() : Color.white.opacity(0.0001))
.edgesIgnoringSafeArea(.all)
}
}
private func getBlurColor() -> Color {
switch blurColor {
case .none:
return Color.white.opacity(0.5)
case .light:
return Color.white.opacity(0.6)
case .dark:
return Color.black.opacity(0.5)
}
}
}

iOS | SwiftUI | Undesirable result to exchange items between TimerView and TaskView through ContentView, #State isn't delivering results

I have three Views exchanging information, ContentView, TimerView and TaskView. I used #Binding in TaskView to bring data from TaskView to ContentView, now I want to use that data to pass into TimerView.
I created a #State variable in ContentView to store the data from TaskView, but when I try to use that #State variable to pass data to TimerView it isn't giving me any desirable results.
Now if you see the TasksView call in ContentView, I get the output for the print statement, but in the main TimerView, it doesn't change from the default I set as #State variable in timerInfo.
Any help is appreciated, thank you.
Code for ContentView ->
//
// ContentView.swift
// ProDActivity
//
// Created by Vivek Pattanaik on 5/27/21.
//
import SwiftUI
struct TimerInfo : Identifiable {
let id = UUID()
// let taskIndex : Int
// let taskProjectName : String
var timerTaskName : String
var timerMinutes : Float
let timerIntervals : Int
var timerPriority : String
var timerShortbreakMinute : Float
var timerLongbreakMinute : Float
var timerLongbreakInterval : Int
}
struct ContentView: View {
init() {
UITabBar.appearance().backgroundColor = UIColor.init(Color("TabBar "))
}
// #State var selection: Tab = .dasboard
#State var timerInfo = TimerInfo(timerTaskName: "Sample Task 1", timerMinutes: 30, timerIntervals: 10, timerPriority: "High Priority", timerShortbreakMinute: 5, timerLongbreakMinute: 15, timerLongbreakInterval: 3)
var body: some View {
TabView {
TimerView(defaultTimeRemaining: self.timerInfo.timerMinutes * 60, timeRemaining: self.timerInfo.timerMinutes * 60)
.tabItem {
Image(systemName: "clock.fill")
Text("Timer")
}.tag(0)
TasksView(didClickTimer: { info in
self.timerInfo.timerTaskName = info.timerTaskName
self.timerInfo.timerMinutes = info.timerMinutes
self.timerInfo.timerPriority = info.timerPriority
self.timerInfo.timerShortbreakMinute = info.timerShortbreakMinute
self.timerInfo.timerLongbreakMinute = info.timerLongbreakMinute
self.timerInfo.timerLongbreakInterval = info.timerLongbreakInterval
print("\(self.timerInfo.timerMinutes)ContentView")
})
.tabItem {
Image(systemName: "doc.plaintext.fill")
Text("Tasks")
}.tag(1)
StatisticsView()
.tabItem {
Image(systemName: "chart.pie.fill")
Text("Statistics")
}.tag(3)
SettingsView()
.tabItem {
Image(systemName: "gearshape.fill")
Text("Settings")
}.tag(4)
}
.font(.headline)
.accentColor(Color("AccentColor"))
.environment(\.colorScheme, .dark)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Code for TasksView ->
//
// TasksView.swift
// ProDActivity
//
// Created by Vivek Pattanaik on 5/30/21.
//
import SwiftUI
struct TaskLabels : Identifiable {
let id = UUID()
// let taskIndex : Int
// let taskProjectName : String
let taskName : String
let taskPriority : String
let taskIntervals : String
let taskMinutes : String
let shortBreakMinutes : String
let longBreakMinutes : String
let longBreakIntervals : String
}
struct TaskRow: View {
let tasks : TaskLabels
var body: some View {
HStack(alignment: .center, spacing: 10) {
Image(tasks.taskPriority)
.frame(width: 40, height: 40, alignment: .center)
VStack(alignment: .leading, spacing:4){
Text(tasks.taskName)
.font(.system(size: 15))
Text("\(tasks.shortBreakMinutes) Min Breaks")
.font(.system(size: 13))
}
.frame(width: 100, height: 20, alignment: .leading)
Spacer()
VStack(alignment: .trailing, spacing:4){
Text("0/\(tasks.taskIntervals)")
Text("\(tasks.taskMinutes) Min Tasks")
.font(.system(size: 13))
}
.frame(width: 90, height: 20, alignment: .trailing)
.padding()
}
}
}
struct TasksView: View {
// #Binding var timeSelected : Float
#State var addTasksModalView: Bool = false
#State var taskLabels : [TaskLabels] = []
var didClickTimer : (TimerInfo) -> ()
var body: some View {
NavigationView{
if taskLabels.count != 0 {
List{
ForEach(taskLabels) { task in
HStack {
Button(action: {
self.didClickTimer(.init(timerTaskName: task.taskName, timerMinutes: Float(task.taskMinutes)!, timerIntervals: Int(task.taskIntervals)!, timerPriority: task.taskPriority, timerShortbreakMinute: Float(task.shortBreakMinutes)!, timerLongbreakMinute: Float(task.longBreakMinutes)!, timerLongbreakInterval: Int(task.longBreakIntervals)!))
print(task.taskMinutes)
}, label: {
TaskRow(tasks: task)
})
}
}
.onDelete(perform: self.deleteRow)
}
.navigationBarTitle("Tasks")
.navigationBarItems(trailing: Button(action: {
self.addTasksModalView = true
}, label: {
Image(systemName: "plus.square.on.square")
.resizable()
.frame(width: 26, height: 26, alignment: .leading)
.foregroundColor(Color.accentColor)
}))
.sheet(isPresented: $addTasksModalView, content: {
AddTasks(addTaskPresented: $addTasksModalView) { tasks in
taskLabels.append(tasks)
}
})
} else {
Text("")
.navigationTitle("Tasks")
.navigationBarTitle("Tasks")
.navigationBarItems(trailing: Button(action: {
self.addTasksModalView = true
}, label: {
Image(systemName: "plus.square.on.square")
.resizable()
.frame(width: 26, height: 26, alignment: .leading)
.foregroundColor(Color.accentColor)
}))
.sheet(isPresented: $addTasksModalView, content: {
AddTasks(addTaskPresented: $addTasksModalView) { tasks in
taskLabels.append(tasks)
}
})
}
}
.environment(\.colorScheme, .dark)
}
private func deleteRow(at indexSet: IndexSet){
self.taskLabels.remove(atOffsets: indexSet)
}
}
//struct TasksView_Previews: PreviewProvider {
// static var previews: some View {
// TasksView()
// }
//}
TimerView Code ->
//
// TimerView.swift
// ProDActivity
//
// Created by Vivek Pattanaik on 5/27/21.
//
import SwiftUI
struct TimerView: View {
let lineWidth : CGFloat = 11
let radius : CGFloat = 150
// to pass into the struct
// #State var taskTime = 300
#State var defaultTimeRemaining : Float
#State var timeRemaining : Float
#State private var isActive = false
#State private var showButtons = false
#State private var stopAlert = false
#State private var pausePressed = false
#State private var stopPressed = false
// #State private var timeRemainingSeconds : CGFloat = 25
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
// self.defaultTimeRemaining = timerInfo.timerMinutes
VStack(spacing : 60) {
ZStack{
RoundedRectangle(cornerRadius: 7)
.frame(width: 300, height: 70)
.foregroundColor(Color("TabBar "))
}
ZStack(alignment: Alignment(horizontal: .center, vertical: .center)) {
Circle()
.stroke(Color.gray, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
.opacity(0.2)
Circle()
.trim(from: 1 - CGFloat(((defaultTimeRemaining-timeRemaining)/defaultTimeRemaining)), to: 1 )
.stroke(Color.accentColor, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
.rotationEffect( .degrees(-90))
.animation(.easeInOut)
// VStack for the timer and seesions
VStack {
// Text("\(Int(timeRemaining)):\(Int(timeRemainingSeconds))")
Text("\(timeString(time: Int(timeRemaining)))")
.font(.system(size: 50)).fontWeight(.medium)
Text("0 of 5 Sessions")
.font(.system(size: 20)).fontWeight(.medium)
}
}.frame(width: radius*2, height: radius*2)
// BEGIN, STOP, PAUSE BUTTONS
HStack(spacing: 25){
if showButtons == false {
Button(action: {
}, label: {
ZStack {
Rectangle()
.frame(width: 176, height: 55, alignment: .center)
.foregroundColor(Color.accentColor)
.cornerRadius(5)
Button(action: {
self.showButtons.toggle()
isActive.toggle()
}, label: {
Text("BEGIN")
.foregroundColor(Color.white)
.font(.system(size: 23).weight(.medium))
.frame(width: 176, height: 55, alignment: .center)
})
}
})
} else if showButtons == true {
HStack {
ZStack {
Rectangle()
.frame(width: 152, height: 55, alignment: .center)
.foregroundColor(Color("LightDark"))
.cornerRadius(5)
.border(Color.accentColor, width: 2)
Button(action: {
self.stopPressed.toggle()
self.pausePressed = true
if isActive == true {
isActive.toggle()
self.stopAlert = true
} else {
self.stopAlert = true
}
}, label: {
Text("STOP")
.foregroundColor(Color.accentColor)
.font(.system(size: 23).weight(.medium))
.frame(width: 152, height: 55, alignment: .center)
})
.alert(isPresented: $stopAlert) {
Alert(title: Text("Are you sure you want to stop?"),
message: Text("This will stop the timer and task associated with it."),
primaryButton: .destructive(Text("Yes"), action: {
self.showButtons = false
timeRemaining = defaultTimeRemaining
}),
secondaryButton: .cancel({
isActive = false
pausePressed = true
})
)
}
}
ZStack {
Rectangle()
.frame(width: 152, height: 55, alignment: .center)
.foregroundColor(pausePressed ? Color.accentColor : Color("LightDark"))
.cornerRadius(5)
.border(pausePressed ? Color.accentColor : Color.accentColor, width: 2)
Button(action: {
pausePressed.toggle()
if pausePressed == true {
isActive = false
} else {
isActive = true
}
}, label: {
Text("\(pausePressed ? "RESUME" : "PAUSE")")
.foregroundColor(pausePressed ? Color("TabBar ") : Color.accentColor)
.font(.system(size: 23).weight(.medium))
.frame(width: 152, height: 55, alignment: .center)
})
}
}
}
}
}.onReceive(timer, perform: { _ in
guard isActive else {return}
if timeRemaining > 0 {
timeRemaining -= 1
} else {
isActive = false
showButtons = false
self.timer.upstream.connect().cancel()
}
})
}
func timeString(time: Int) -> String {
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
return String(format:"%02i:%02i", minutes, seconds)
}
}
//struct TimerView_Previews: PreviewProvider {
// static var previews: some View {
// TimerView()
// }
//}
//func buttonChange(){
//
//}

Custom Segmented Controller SwiftUI Frame Issue

I would like to create a custom segmented controller in SwiftUI, and I found one made from this post. After slightly altering the code and putting it into my ContentView, the colored capsule would not fit correctly.
Here is an example of my desired result:
This is the result when I use it in ContentView:
CustomPicker.swift:
struct CustomPicker: View {
#State var selectedIndex = 0
var titles = ["Item #1", "Item #2", "Item #3", "Item #4"]
private var colors = [Color.red, Color.green, Color.blue, Color.purple]
#State private var frames = Array<CGRect>(repeating: .zero, count: 4)
var body: some View {
VStack {
ZStack {
HStack(spacing: 4) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.titles[index])
.foregroundColor(.black)
.font(.system(size: 16, weight: .medium, design: .default))
.bold()
}.padding(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)).background(
GeometryReader { geo in
Color.clear.onAppear { self.setFrame(index: index, frame: geo.frame(in: .global)) }
}
)
}
}
.background(
Capsule().fill(
self.colors[self.selectedIndex].opacity(0.4))
.frame(width: self.frames[self.selectedIndex].width,
height: self.frames[self.selectedIndex].height, alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
, alignment: .leading
)
}
.animation(.default)
.background(Capsule().stroke(Color.gray, lineWidth: 3))
}
}
func setFrame(index: Int, frame: CGRect) {
self.frames[index] = frame
}
}
ContentView.swift:
struct ContentView: View {
#State var itemsList = [Item]()
func loadData() {
if let url = Bundle.main.url(forResource: "Data", withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let jsonData = try decoder.decode(Response.self, from: data)
for post in jsonData.content {
self.itemsList.append(post)
}
} catch {
print("error:\(error)")
}
}
}
var body: some View {
NavigationView {
VStack {
Text("Item picker")
.font(.system(.title))
.bold()
CustomPicker()
Spacer()
ScrollView {
VStack {
ForEach(itemsList) { item in
ItemView(text: item.text, username: item.username)
.padding(.leading)
}
}
}
.frame(height: UIScreen.screenHeight - 224)
}
.onAppear(perform: loadData)
}
}
}
Project file here
The problem with the code as-written is that the GeometryReader value is only sent on onAppear. That means that if any of the views around it change and the view is re-rendered (like when the data is loaded), those frames will be out-of-date.
I solved this by using a PreferenceKey instead, which will run on each render:
struct CustomPicker: View {
#State var selectedIndex = 0
var titles = ["Item #1", "Item #2", "Item #3", "Item #4"]
private var colors = [Color.red, Color.green, Color.blue, Color.purple]
#State private var frames = Array<CGRect>(repeating: .zero, count: 4)
var body: some View {
VStack {
ZStack {
HStack(spacing: 4) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.titles[index])
.foregroundColor(.black)
.font(.system(size: 16, weight: .medium, design: .default))
.bold()
}
.padding(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16))
.measure() // <-- Here
.onPreferenceChange(FrameKey.self, perform: { value in
self.setFrame(index: index, frame: value) //<-- this will run each time the preference value changes, will will happen any time the frame is updated
})
}
}
.background(
Capsule().fill(
self.colors[self.selectedIndex].opacity(0.4))
.frame(width: self.frames[self.selectedIndex].width,
height: self.frames[self.selectedIndex].height, alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
, alignment: .leading
)
}
.animation(.default)
.background(Capsule().stroke(Color.gray, lineWidth: 3))
}
}
func setFrame(index: Int, frame: CGRect) {
print("Setting frame: \(index): \(frame)")
self.frames[index] = frame
}
}
struct FrameKey : PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
extension View {
func measure() -> some View {
self.background(GeometryReader { geometry in
Color.clear
.preference(key: FrameKey.self, value: geometry.frame(in: .global))
})
}
}
Note that the original .background call was taken out and was replaced with .measure() and .onPreferenceChange -- look for where the //<-- Here note is.
Besides that and the PreferenceKey and View extension, nothing else is changed.

SwiftUI adding personal alert view in NavigationView, the back button doesn't work with Xcode 12 iOS14

I have an extension (.alertLinearProgressBar) like an .alert view to display a unzipping progress.
Before iOS 14 was working well, now if I put this alert, the navigation back button doesn't work anymore (if I drag right, is working and it come back to the list, so I guess the problem is a conflict with back button bar, only that button doesn't work, other button in top bar are working).
Anyone knows this problem?
import SwiftUI
var chatData = ["1","2","3","4"]
struct ContentView: View {
#State private var selection = 0
#State private var isShowingAlert = false
var body: some View {
TabView (selection: $selection) {
NavigationView {
ZStack (alignment: .bottom) {
MasterView()
.navigationBarTitle(Text("Chat"))
.navigationBarItems(
leading: EditButton(),
trailing: Button(
action: {
//
}
) {
Image(systemName: "plus.circle")
.contentShape(Rectangle())
})
.background(Color.clear)
}
}
//IF I comment this line below, the navigation is working well
.alertLinearProgressBar(isShowing: self.$isShowingAlert, progressValue: .constant(0.5), barHeight: 8, loadingText: .constant(""), titleText: .constant(NSLocalizedString("unzipping", comment: "")),isShowingActivityIndicator: .constant(true)).offset(x: 0, y: 1)
}
}
}
struct MasterView: View {
var body: some View {
ZStack {
List {
ForEach(chatData, id: \.self) { chat in
NavigationLink(
destination:
//Test2()
DetailView(text: chat)
) {
ChatRow(text: chat)//, progressValue: self.$progressValue)
}
}
}
}
}
}
struct ChatRow: View {
var text: String
var body: some View {
Text(text)
}
}
struct DetailView: View {
var text: String
var body: some View {
Text(text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AlertLinearProgressBar<Presenting>: View where Presenting: View {
#Binding var isShowing: Bool
#Binding var progressValue:Float
#State var barHeight: Int
#Binding var loadingText: String
#Binding var titleText: String
#Binding var isShowingProgressBar: Bool
#Binding var isShowingActivityIndicator: Bool
let presenting: () -> Presenting
var body: some View {
GeometryReader { geometry in
self.presenting()
.blur(radius: self.isShowing ? 2 : 0).offset(y:1)
.disabled(self.isShowing)
ZStack {
Rectangle()
.frame(width: geometry.size.width * 0.8,
height: self.titleText == "" ? 70:100)
.foregroundColor(Color.white)
.cornerRadius(15)
.shadow(radius: 20)
.overlay(
GeometryReader { geometry in
VStack {
if self.titleText != "" {
Text(self.titleText)
.bold()
.offset(x: 0, y: 0)
.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
}
HStack {
Text("\(self.loadingText) " + "\(self.isShowingProgressBar ? self.progressValue.getPercentage(to: 1):"")")
.font(.caption)
ActivityIndicator(isAnimating: .constant(true), isShowing: self.$isShowingActivityIndicator, style: .medium)
}
//.font(.system(size: 13))
Spacer()
.frame(height:6)
ZStack(alignment: .leading) {
Rectangle()
.frame(width: geometry.size.width, height: CGFloat(self.barHeight))
.opacity(0.3)
.foregroundColor(Color(UIColor.systemTeal))
.cornerRadius(5.0)
Rectangle()
.frame(width: min(CGFloat(self.progressValue)*geometry.size.width, geometry.size.width), height: CGFloat(self.barHeight))
.foregroundColor(Color.blue)
.animation(.linear)
.cornerRadius(5.0)
}.opacity(self.isShowingProgressBar ? 1 : 0)
}
}
.padding(EdgeInsets(top: 0, leading: 15, bottom: 0, trailing: 15))
)
.padding()
}
.frame(width: self.isShowing ? geometry.size.width:0,
height: self.isShowing ? geometry.size.height:0)
.transition(.slide)
.opacity(self.isShowing ? 1 : 0)
}
}
}
extension Float {
//number of decimal
func round(to places: Int) -> Float {
let divisor = pow(10.0, Float(places))
return (self * divisor).rounded() / divisor
}
func getPercentage(to digits: Int) -> String {
if self >= 1 {
return String(Int(self * 100)) + "%"
}
return String(format: "%.\(digits)f", self * 100) + "%"
}
}
extension View {
func alertLinearProgressBar(isShowing: Binding<Bool>,
progressValue: Binding<Float>,
barHeight: Int, loadingText: Binding<String>, titleText: Binding<String>=Binding.constant(""), isShowingProgressBar: Binding<Bool>=Binding.constant(true), isShowingActivityIndicator:Binding<Bool>=Binding.constant(false)) -> some View {
AlertLinearProgressBar(isShowing: isShowing, progressValue: progressValue, barHeight: barHeight, loadingText: loadingText, titleText: titleText, isShowingProgressBar: isShowingProgressBar, isShowingActivityIndicator:isShowingActivityIndicator, presenting: {self})
}
}
struct ActivityIndicator: UIViewRepresentable {
#Binding var isAnimating: Bool
#Binding var isShowing: Bool
let style: UIActivityIndicatorView.Style
var color:UIColor?
func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
uiView.isHidden = isShowing ? false:true
if color != nil {
uiView.color = color!
}
}
}
It looks like your AlertLinearProgressBar even with the opacity set to 0 blocks the NavigationBar.
You can see that the overlay's position when hidden is in the top left corner and overlapping with the navigation bar (try setting .opacity(self.isShowing ? 1 : 0.5)).
What you can do is to truly hide it with the hidden modifier.
Here is a possible solution using the if modifier:
struct AlertLinearProgressBar<Presenting>: View where Presenting: View {
// ...
var body: some View {
GeometryReader { geometry in
ZStack {
// ...
}
.frame(width: self.isShowing ? geometry.size.width : 0,
height: self.isShowing ? geometry.size.height : 0)
.transition(.slide)
.opacity(self.isShowing ? 1 : 0.5)
.if(!isShowing) {
$0.hidden() // use `hidden` here
}
}
}
}
extension View {
#ViewBuilder func `if`<T>(_ condition: Bool, transform: (Self) -> T) -> some View where T: View {
if condition {
transform(self)
} else {
self
}
}
}
Alternatively you can just display the view conditionally:
struct AlertLinearProgressBar<Presenting>: View where Presenting: View {
//...
var body: some View {
GeometryReader { geometry in
self.presenting()
.blur(radius: self.isShowing ? 2 : 0).offset(y: 1)
.disabled(self.isShowing)
if isShowing {
// ...
}
}
}
}