I've got the following view:
The Swift code looks like this:
struct TestView: View {
let options = [" ", "1", "2", "3", "4", "5", "6"]
#State var selectedIndex: Int = 0
var body: some View {
HStack(spacing: 0) {
Text("One")
Spacer()
Picker(selection: $selectedIndex, label: Text(options[selectedIndex])) {
ForEach(0 ..< options.count) {
Text(options[$0])
}
}
.background(Color.red)
.pickerStyle(MenuPickerStyle())
}
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
.background(Color.yellow)
}
}
When clicking on the red square, the Picker will be opened:
How can I extend the touch area of the red rectangle to also include the entire yellow area?
#DonMag's answer stopped working with iOS 15. Here's an updated answer that does work. Technically, it does not use Slider, the behavior is the same though. Instead a Menu is used.
struct PickerTestView: View {
let options = [" ", "1", "2", "3", "4", "5", "6"]
let optionNames = [" ", "One", "Two", "Three", "Four", "Five", "Six"]
#State var selectedIndex: Int = 0
var body: some View {
ZStack {
HStack(spacing: 0) {
Text(optionNames[selectedIndex])
Spacer()
}
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
.background(Color.yellow)
HStack(spacing: 0) {
Menu {
ForEach(0 ..< options.count) {
let index = $0
Button("\(options[index])") {
selectedIndex = index
}
}
} label: {
Label("", image: "")
.labelStyle(TitleOnlyLabelStyle())
.frame(maxWidth: .infinity)
}
}
}
}
}
struct PickerTestView_Previews: PreviewProvider {
static var previews: some View {
PickerTestView()
}
}
Let's see when Apple decides to break this implementation.
Not sure this is exactly what you're after, but give it a try (initial view is a "blank" yellow bar):
import SwiftUI
struct PickerTestView: View {
let options = [" ", "1", "2", "3", "4", "5", "6"]
let optionNames = [" ", "One", "Two", "Three", "Four", "Five", "Six"]
#State var selectedIndex: Int = 0
var body: some View {
ZStack {
HStack(spacing: 0) {
Text(optionNames[selectedIndex])
Spacer()
}
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
.background(Color.yellow)
HStack(spacing: 0) {
Picker(selection: $selectedIndex, label: Text(" ").frame(maxWidth: .infinity), content: {
ForEach(0 ..< options.count) {
Text(options[$0])
}
})
.pickerStyle(MenuPickerStyle())
}
}
}
}
struct PickerTestView_Previews: PreviewProvider {
static var previews: some View {
PickerTestView()
}
}
On launch:
Tap anywhere on the yellow bar:
After selecting "3":
Related
I have followed a tutorial in SWIFT UI (only just started using it) and I am trying to open new views using the same logic applied so far. Basically there is a tab bar with 5 views (Search ,home etc...) which works opening a new view with each tabbar item, however in my homeview page I have some button cards that I want to open a new view. I can get the text for selectedSection to work but it shows the Text over the top of the homeview. How can I get it to open a new view entirely?
Here is my content view:
struct ContentView: View {
#AppStorage("selectedTab") var selectedTab: Tab = .home
#AppStorage("selectedSection") var selectedSection: Features = .calculators
#State var isOpen = false
#State var show = false
let button = RiveViewModel(fileName: "menu_button", stateMachineName: "State
Machine", autoPlay: false)
var body: some View {
ZStack {
Color("Background 2").ignoresSafeArea()
SideMenu()
.opacity(isOpen ? 1 : 0)
.offset(x: isOpen ? 0 : -300)
.rotation3DEffect(.degrees(isOpen ? 0 : 30), axis: (x: 0, y: 1, z: 0))
Group{
switch selectedTab {
case .home:
HomeView()
case .search:
Text("Search")
case .star:
Text("Favorites")
case .bell:
Text("Bell")
case .user:
Text("User")
}
switch selectedSection {
case .calculators:
Text("Calculators")
case .projects:
Text("Projects")
case .kvFinder:
Text("kv Finder")
}
}
And my home view:
var content: some View {
VStack(alignment: .leading, spacing: 0) {
Text("Welcome")
.customFont(.largeTitle)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(sections) { section in
Button {
selectedSection = section.features
} label : {
VCard(section: section)
}
}
}
And here is my VCard:
struct Section: Identifiable {
var id = UUID()
var title: String
var subtitle: String
var caption: String
var color: Color
var image: Image
var features: Features
}
var sections = [
Section(title: "TAB Calculations", subtitle: "Find all basic and advanced HVAC
calculations", caption: "3 sections - over 40 calculators", color: Color(hex:
"7850F0"), image: Image(systemName: "x.squareroot"), features: .calculators),
Section(title: "Upcoming Projects", subtitle: "Find upcoming and current
commissioning projects.", caption: "Over 150 projects", color: Color(hex: "6792FF"),
image: Image(systemName: "folder.fill.badge.plus"), features: .projects),
Section(title: "Valve Kv Finder", subtitle: "Quickly determine valve flow rates from
brands such as Oventropp, IMI TA and Danfoss", caption: "150 tables", color:
Color(hex: "005FE7"), image: Image(systemName: "magnifyingglass"), features:
.kvFinder)
]
enum Features: String {
case calculators
case projects
case kvFinder
}
You can use NavigationStack API if your minimum app deployment is 16+. otherwise, you may use the old NavigationView.
You can find the migration document here.
struct ContentView: View {
#State var path: [YourDestinations] = []
var body: some View {
TabView {
VStack {
NavigationStack(path: $path) { // <= here
VStack {
NavigationLink("Card 1", value: YourDestinations.place1)
NavigationLink("Card 1", value: YourDestinations.place2)
NavigationLink("Card 1", value: YourDestinations.place3)
}
.navigationDestination(for: YourDestinations.self) { destination in
switch destination {
case .place1:
Text("Detination 1")
.foregroundColor(.yellow)
case .place2:
Text("Detination 2")
.foregroundColor(.green)
case .place3:
Text("Detination 3")
.foregroundColor(.gray)
}
}
}
}
.tabItem({
Text("Tab 1")
})
Text("Hello, world!")
.padding()
.tabItem({
Text("Tab 2")
})
Text("Hello, world!")
.padding()
.tabItem({
Text("Tab 3")
})
}
}
}
I have list of custom fields in a custom form view. The form view is loaded in the content view, which will inform the form view to move to the next field, when the user taps on "Next" button.
The question is how the form view will make the next custom field focused, when the moveToNextField() function called?
This is how my form looks like
Here's the code for the custom field
enum InputFieldType {
case text, number, dropdown
}
struct CustomField: View {
let tag: Int
let type: InputFieldType
let title: String
var dropdownItems: Array<String> = []
var placeholder: String = ""
#State var text: String = ""
#State var enabled: Bool = true
#FocusState private var focusField: Bool
private let dropdownImage = Image(systemName: "chevron.down")
#State private var showDropdown: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 8.0) {
Text(title)
.foregroundColor(.gray)
.frame(alignment: .leading)
ZStack(alignment: .leading) {
// The placeholder view
Text(placeholder).foregroundColor( enabled ? .gray.opacity(0.3) : .gray.opacity(0.5))
.opacity(text.isEmpty ? 1 : 0)
.padding(.horizontal, 8)
// Text field
TextField("", text: $text)
.disabled(!enabled)
.frame(height: 44)
.textInputAutocapitalization(.sentences)
.foregroundColor(.white)
.padding(.horizontal, 8)
.keyboardType( type == .number ? .decimalPad : .default)
.focused($focusField)
}.background(Color.red.opacity(0.1))
.cornerRadius(5)
}
}
}
Here's the code for the form view
struct FormView: View {
func moveToNextField() -> Bool {
return false
}
var body: some View {
VStack {
ScrollView(.vertical) {
VStack(spacing: 24) {
CustomField(tag: 0, type: .text, title: "First name", placeholder: "John", text: "", enabled: false)
CustomField(tag: 1, type: .text, title: "Surname", placeholder: "Mike", text: "")
CustomField(tag: 2, type: .text, title: "Gender (Optional)", placeholder: "Optional", text: "")
CustomField(tag: 3, type: .dropdown, title: "Body type", dropdownItems: ["1", "2", "3"], placeholder: "Skinny", text: "")
CustomField(tag: 4, type: .number, title: "Year of birth", placeholder: "2000", text: "")
Spacer()
}
}
}.onTapGesture {
}
.background(Color.clear)
.padding(.horizontal, 16)
}
}
The code in the Content view
struct ContentView: View {
let formView = FormView()
var body: some View {
VStack {
Spacer(minLength: 30)
formView
.padding(.vertical)
Button("Next") {
if formView.moveToNextField() {
return
}
// validate the form
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .center)
.background(Color.secondary)
.cornerRadius(5)
.padding(.horizontal, 16)
Spacer(minLength: 20)
}.background(Color.primary)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().preferredColorScheme(.dark)
}
}
Declare a FocusState var for each field. Then turn on the state of the field where you want to move the focus.
I found my issue, I was using #State to track the current focused field, which is getting reset every time the view changes.
So I had to us #ObservableObject with #Published property.
Here is my final working code.
Custom field
enum InputFieldType {
case text, number, dropdown
}
struct CustomField: View {
let tag: Int
let type: InputFieldType
let title: String
var dropdownItems: Array<String> = []
var placeholder: String = ""
#State var text: String = ""
#State var enabled: Bool = true
#Binding var focusTag: Int
#FocusState private var focusField: Bool
private let dropdownImage = Image(systemName: "chevron.down")
#State private var showDropdown: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 8.0) {
Text(title)
.foregroundColor(.gray)
.frame(alignment: .leading)
ZStack(alignment: .leading) {
// The placeholder view
Text(placeholder).foregroundColor( enabled ? .gray.opacity(0.3) : .gray.opacity(0.5))
.opacity(text.isEmpty ? 1 : 0)
.padding(.horizontal, 8)
// Text field
TextField("", text: $text)
.disabled(!enabled)
.frame(height: 44)
.textInputAutocapitalization(.sentences)
.foregroundColor(.white)
.padding(.horizontal, 8)
.keyboardType( type == .number ? .decimalPad : .default)
.onChange(of: focusTag, perform: { newValue in
focusField = newValue == tag
})
.focused($focusField)
.onChange(of: focusField, perform: { newValue in
if type != .dropdown, newValue, focusTag != tag {
focusTag = tag
}
})
}.background(Color.red.opacity(0.1))
.cornerRadius(5)
}
}
}
Form view
fileprivate class FocusStateObserver: ObservableObject {
#Published var focusFieldTag: Int = -1
}
struct FormView: View {
#ObservedObject private var focusStateObserver = FocusStateObserver()
func moveToNextField() -> Bool {
if focusStateObserver.focusFieldTag < 4 {
switch focusStateObserver.focusFieldTag {
case 0:
focusStateObserver.focusFieldTag = 1
case 1:
focusStateObserver.focusFieldTag = 2
case 2:
focusStateObserver.focusFieldTag = 4
default:
break
}
return true
}
return false
}
var body: some View {
VStack {
ScrollView(.vertical) {
VStack(spacing: 24) {
CustomField(tag: 0, type: .text, title: "First name", placeholder: "John", text: "", enabled: false, focusTag: $focusStateObserver.focusFieldTag)
CustomField(tag: 1, type: .text, title: "Surname", placeholder: "Mike", text: "", focusTag: $focusStateObserver.focusFieldTag)
CustomField(tag: 2, type: .text, title: "Gender (Optional)", placeholder: "Optional", text: "", focusTag: $focusStateObserver.focusFieldTag)
CustomField(tag: 3, type: .dropdown, title: "Body type", dropdownItems: ["1", "2", "3"], placeholder: "Skinny", text: "", focusTag: $focusStateObserver.focusFieldTag)
CustomField(tag: 4, type: .number, title: "Year of birth", placeholder: "2000", text: "", focusTag: $focusStateObserver.focusFieldTag)
Spacer()
}
}
}.onTapGesture {
endEditing()
}
.background(Color.clear)
.padding(.horizontal, 16)
}
}
extension View {
func endEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Content view
struct ContentView: View {
let formView = FormView()
var body: some View {
VStack {
Spacer(minLength: 30)
formView
.padding(.vertical)
Button("Next") {
if formView.moveToNextField() {
return
}
endEditing()
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .center)
.background(Color.secondary)
.cornerRadius(5)
.padding(.horizontal, 16)
Spacer(minLength: 20)
}.background(Color.primary)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().preferredColorScheme(.dark)
}
}
I am using SwiftUI to build a feature where a user can click on a button to create rows and each row has multiple views (Textfields, Radio buttons...) [see image below]
I created a swift class DynamicSkills where I handle the creation of the items and the "Add" button, but when I click "Add" the App crashes completely, below is my code so far and an image showing how it should be, I just started with SwiftUI and I am not fully grasping the concept of Binding<>.
What should I do exactly to get it up and running?
//DynamicSkills.swift
struct SkillListEditor: View {
#Binding var evalList: [String]
#Binding var skillList: [String]
func getBindingS(forIndex index: Int) -> Binding<String> {
return Binding<String>(get: { skillList[index] },
set: { skillList[index] = $0 })
}
func getBindingE(forIndex index: Int) -> Binding<String> {
return Binding<String>(get: { evalList[index] },
set: { evalList[index] = $0 })
}
var body: some View {
ForEach(0..<skillList.count, id: \.self) { index in
ListItem(skillEvaluation: getBindingE(forIndex: index), text: getBindingS(forIndex: index)) { self.skillList.remove(at: index) }
}
AddButton{ self.skillList.append("") }
}
}
fileprivate struct ListItem: View {
#Binding var skillEvaluation: String
#Binding var text: String
var removeAction: () -> Void
var body: some View {
VStack(alignment: .leading){
Spacer().frame(height: 8)
HStack{
Button(action: removeAction) {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
.padding(.horizontal)
}
TextField("Skill", text: $text)
.padding(.all)
.background(Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0))
.cornerRadius(6)
}
HStack{
Text("Evaluation").font(.callout)
Spacer()
}
HStack{
RadioButtonField(id: "1", label: "1", isMarked: $skillEvaluation.wrappedValue == "1" ? true : false) { selected in
self.skillEvaluation = selected
}
Spacer()
RadioButtonField(id: "2", label: "2", isMarked: $skillEvaluation.wrappedValue == "2" ? true : false) { selected in
self.skillEvaluation = selected
}
Spacer()
RadioButtonField(id: "3", label: "3", isMarked: $skillEvaluation.wrappedValue == "3" ? true : false) { selected in
self.skillEvaluation = selected
}
Spacer()
RadioButtonField(id: "4", label: "4", isMarked: $skillEvaluation.wrappedValue == "4" ? true : false) { selected in
self.skillEvaluation = selected
}
Spacer()
RadioButtonField(id: "5", label: "5", isMarked: $skillEvaluation.wrappedValue == "5" ? true : false) { selected in
self.skillEvaluation = selected
}
}
}
}
}
fileprivate struct AddButton: View {
var addAction: () -> Void
var body: some View {
HStack{
Spacer()
Button(action: addAction) {
Text("add skill").padding(.horizontal, 12).padding(.vertical, 6).background(.white).foregroundColor(Color("PassioGreen")).overlay(RoundedRectangle(cornerRadius: 25).stroke(Color("PassioGreen"), lineWidth: 2))
}
}.padding(.top, 14)
}
}
And in my main screen in ContentView I am showing it this way:
//ContentView.swift
#State var skills = [String]()
#State var skillsEvals = [String]()
struct ContentView: View {
NavigationView{
VStack{
HStack{
Text("Skills").font(.headline)
Spacer()
}
HStack{
Text("Add up to 5 skills").font(.callout)
Spacer()
}
SkillListEditor(evalList: $skillsEvals, skillList: $skills)
}
}
}
Update: this is the custom radio button class CustomRadio.swift
//Custom Radio button used:
struct RadioButtonField: View {
let id: String
let label: String
let isMarked: Bool
let callback: (String)->()
init(
id: String,
label:String,
isMarked: Bool = false,
callback: #escaping (String)->()
) {
self.id = id
self.label = label
self.isMarked = isMarked
self.callback = callback
}
var body: some View {
Button(action:{
self.callback(self.id)
}) {
VStack(alignment: .center) {
Text(label).foregroundColor(self.isMarked ? .white : .black).bold().padding(.horizontal, 8).padding(.vertical, 8)
}
}
.padding()
.background(self.isMarked ? Color("PassioGreen") : Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0))
.cornerRadius(10)
}
}
Your AddButton action appends an item to skillList, but does not append an item to evalList. So when ListItem uses its skillEvaluation binding, the binding's getter tries to access an element of evalList that doesn't exist.
Try this:
AddButton {
self.evalList.append("")
self.skillList.append("")
}
Below is my code to create a standard segmented control.
struct ContentView: View {
#State private var favoriteColor = 0
var colors = ["Red", "Green", "Blue"]
var body: some View {
VStack {
Picker(selection: $favoriteColor, label: Text("What is your favorite color?")) {
ForEach(0..<colors.count) { index in
Text(self.colors[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Value: \(colors[favoriteColor])")
}
}
}
My question is how could I modify it to have a customized segmented control where I can have the boarder rounded along with my own colors, as it was somewhat easy to do with UIKit? Has any one done this yet.
I prefect example is the Uber eats app, when you select a restaurant you can scroll to the particular portion of the menu by selecting an option in the customized segmented control.
Included are the elements I'm looking to have customized:
* UPDATE *
Image of the final design
Is this what you are looking for?
import SwiftUI
struct CustomSegmentedPickerView: View {
#State private var selectedIndex = 0
private var titles = ["Round Trip", "One Way", "Multi-City"]
private var colors = [Color.red, Color.green, Color.blue]
#State private var frames = Array<CGRect>(repeating: .zero, count: 3)
var body: some View {
VStack {
ZStack {
HStack(spacing: 10) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.titles[index])
}.padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20)).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))
Picker(selection: self.$selectedIndex, label: Text("What is your favorite color?")) {
ForEach(0..<self.titles.count) { index in
Text(self.titles[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Value: \(self.titles[self.selectedIndex])")
Spacer()
}
}
func setFrame(index: Int, frame: CGRect) {
self.frames[index] = frame
}
}
struct CustomSegmentedPickerView_Previews: PreviewProvider {
static var previews: some View {
CustomSegmentedPickerView()
}
}
If I'm following the question aright the starting point might be something like the code below. The styling, clearly, needs a bit of attention. This has a hard-wired width for segments. To be more flexible you'd need to use a Geometry Reader to measure what was available and divide up the space.
struct ContentView: View {
#State var selection = 0
var body: some View {
let item1 = SegmentItem(title: "Some Way", color: Color.blue, selectionIndex: 0)
let item2 = SegmentItem(title: "Round Zip", color: Color.red, selectionIndex: 1)
let item3 = SegmentItem(title: "Multi-City", color: Color.green, selectionIndex: 2)
return VStack() {
Spacer()
Text("Selected Item: \(selection)")
SegmentControl(selection: $selection, items: [item1, item2, item3])
Spacer()
}
}
}
struct SegmentControl : View {
#Binding var selection : Int
var items : [SegmentItem]
var body : some View {
let width : CGFloat = 110.0
return HStack(spacing: 5) {
ForEach (items, id: \.self) { item in
SegmentButton(text: item.title, width: width, color: item.color, selectionIndex: item.selectionIndex, selection: self.$selection)
}
}.font(.body)
.padding(5)
.background(Color.gray)
.cornerRadius(10.0)
}
}
struct SegmentButton : View {
var text : String
var width : CGFloat
var color : Color
var selectionIndex = 0
#Binding var selection : Int
var body : some View {
let label = Text(text)
.padding(5)
.frame(width: width)
.background(color).opacity(selection == selectionIndex ? 1.0 : 0.5)
.cornerRadius(10.0)
.foregroundColor(Color.white)
.font(Font.body.weight(selection == selectionIndex ? .bold : .regular))
return Button(action: { self.selection = self.selectionIndex }) { label }
}
}
struct SegmentItem : Hashable {
var title : String = ""
var color : Color = Color.white
var selectionIndex = 0
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
None of the above solutions worked for me as the GeometryReader returns different values once placed in a Navigation View that throws off the positioning of the active indicator in the background. I found alternate solutions, but they only worked with fixed length menu strings. Perhaps there is a simple modification to make the above code contributions work, and if so, I would be eager to read it. If you're having the same issues I was, then this may work for you instead.
Thanks to inspiration from a Reddit user "End3r117" and this SwiftWithMajid article, https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/, I was able to craft a solution. This works either inside or outside of a NavigationView and accepts menu items of various lengths.
struct SegmentMenuPicker: View {
var titles: [String]
var color: Color
#State private var selectedIndex = 0
#State private var frames = Array<CGRect>(repeating: .zero, count: 5)
var body: some View {
VStack {
ZStack {
HStack(spacing: 10) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: {
print("button\(index) pressed")
self.selectedIndex = index
}) {
Text(self.titles[index])
.foregroundColor(color)
.font(.footnote)
.fontWeight(.semibold)
}
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5))
.modifier(FrameModifier())
.onPreferenceChange(FramePreferenceKey.self) { self.frames[index] = $0 }
}
}
.background(
Rectangle()
.fill(self.color.opacity(0.4))
.frame(
width: self.frames[self.selectedIndex].width,
height: 2,
alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX, y: self.frames[self.selectedIndex].height)
, alignment: .leading
)
}
.padding(.bottom, 15)
.animation(.easeIn(duration: 0.2))
Text("Value: \(self.titles[self.selectedIndex])")
Spacer()
}
}
}
struct FramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
struct FrameModifier: ViewModifier {
private var sizeView: some View {
GeometryReader { geometry in
Color.clear.preference(key: FramePreferenceKey.self, value: geometry.frame(in: .global))
}
}
func body(content: Content) -> some View {
content.background(sizeView)
}
}
struct NewPicker_Previews: PreviewProvider {
static var previews: some View {
VStack {
SegmentMenuPicker(titles: ["SuperLongValue", "1", "2", "Medium", "AnotherSuper"], color: Color.blue)
NavigationView {
SegmentMenuPicker(titles: ["SuperLongValue", "1", "2", "Medium", "AnotherSuper"], color: Color.red)
}
}
}
}
Is there a way to remove the default list row collapse animation
without using .animation(nill) modifier on the list itself?
as you can see in the clip below, I've implemented an animation modifier on the list, but the default list collapse animation kinda disrupts the desired animation.
https://gfycat.com/cheerywelloffalleycat
I've updated the code below so you could run it on your Xcode without any dependencies.
import SwiftUI
struct CurrencyComparison: View {
#State var mainArray = ["10", "10","10", "10", "10", "10", "10", "10", "10", "10"]
#State var array = ["10", "10","10", "10", "10", "10", "10", "10", "10", "10"]
#State var secondArray = ["20", "20","20", "20", "20", "20", "20", "20", "20", "20"]
#State var hide = false
#State var direction = false
#State var triggerAnimation: Bool
var body: some View {
VStack {
List (self.mainArray, id:\.self) { item in
Text(item)
.foregroundColor(Color.black)
.frame(width: 40, height: 80)
.padding(.leading, 80)
.isHidden(self.hide)
Spacer()
Text(item)
.foregroundColor(Color.black)
.frame(width: 40, height: 40)
.padding(.trailing, 80)
.isHidden(self.hide)
}
.background(LinearGradient(gradient: Gradient(colors: [Color.blue, Color.red]), startPoint: .center, endPoint: .center))
.animation(Animation.spring().delay(triggerAnimation ? 0 : 0.4))
.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .move(edge: .trailing)), removal: .move(edge: .trailing)))
.cornerRadius(30)
.padding(.top, 30)
.padding(.bottom, 30)
.shadow(radius: 10)
.gesture(
DragGesture(minimumDistance: 50)
.onEnded { value in
self.hide.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() ) {
if self.mainArray == self.array {
self.mainArray = self.secondArray
} else {
self.mainArray = self.array
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.hide.toggle()
}
}
)
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height + 50, alignment: .top)
.background(Color.gray.aspectRatio(contentMode: .fill))
.padding(.top, 120)
}
}
struct CurrencyComparison_Previews: PreviewProvider {
#State static var staticBool = true
static var previews: some View {
CurrencyComparison(triggerAnimation: true)
}
}
extension View {
func isHidden(_ bool: Bool) -> some View {
modifier(HiddenModifier(isHidden: bool))
}
}
private struct HiddenModifier: ViewModifier {
fileprivate let isHidden: Bool
fileprivate func body(content: Content) -> some View {
Group {
if isHidden {
content.hidden()
} else {
content
.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .slide), removal: .move(edge: .trailing)))
}
}
}
}
Okay so I've managed to find a solution that works. By changing the opacity of the list items when the list data source change happens.
Setting it to 0 when the undesired default animation play, and bringing it back to 1 when my the desired animations start playing.
That way I can hide the default list animation without removing my own desired animations :)
import SwiftUI
struct CurrencyComparison: View {
#State var mainArray = ["10", "10","10", "10", "10", "10", "10", "10", "10", "10"]
#State var array = ["10", "10","10", "10", "10", "10", "10", "10", "10", "10"]
#State var secondArray = ["20", "20","20", "20", "20", "20", "20", "20", "20", "20"]
#State var toggleHide = false
#State var direction = false
#State var triggerAnimation: Bool
var body: some View {
VStack {
List (self.mainArray, id:\.self) { item in
Text(item)
.foregroundColor(Color.black)
.frame(width: 40, height: 80)
.padding(.leading, 80)
.isHidden(self.toggleHide)
.opacity(self.toggleHide ? 0 : 1)
Spacer()
Text(item)
.foregroundColor(Color.black)
.frame(width: 40, height: 40)
.padding(.trailing, 80)
.isHidden(self.toggleHide)
.opacity(self.toggleHide ? 0 : 1)
}
.background(LinearGradient(gradient: Gradient(colors: [Color.blue, Color.red]), startPoint: .center, endPoint: .center))
.animation(Animation.spring().delay(triggerAnimation ? 0 : 0.4))
.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .move(edge: .trailing)), removal: .move(edge: .trailing)))
.cornerRadius(30)
.padding(.top, 30)
.padding(.bottom, 30)
.shadow(radius: 10)
.gesture(
DragGesture(minimumDistance: 50)
.onEnded { value in
self.toggleHide.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
if self.mainArray == self.array {
self.mainArray = self.secondArray
} else {
self.mainArray = self.array
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.9) {
self.toggleHide.toggle()
}
}
)
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height + 50, alignment: .top)
.background(Color.gray.aspectRatio(contentMode: .fill))
.padding(.top, 120)
}
}
struct CurrencyComparison_Previews: PreviewProvider {
#State static var staticBool = true
static var previews: some View {
CurrencyComparison(triggerAnimation: true)
}
}
extension View {
func isHidden(_ bool: Bool) -> some View {
modifier(HiddenModifier(isHidden: bool))
}
}
private struct HiddenModifier: ViewModifier {
fileprivate let isHidden: Bool
fileprivate func body(content: Content) -> some View {
Group {
if isHidden {
content.hidden()
} else {
content
.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .slide), removal: .move(edge: .trailing)))
}
}
}
}