iOS 14 menu picker style's label becomes dim sometimes when changing - ios

I have a Picker in my SwiftUI View with the new MenuPickerStyle.
As you can see, the label of the picker is same of the options, and it becomes dim when changing from one option to another.
It looks like it is not tappable, but when tapping it does the required job.
Here's my code. It is just a very simple picker implementation.
struct MenuPicker: View {
#State var selection: String = "one"
var array: [String] = ["one", "two", "three", "four", "five"]
var body: some View {
Picker(selection: $selection, label: Text(selection).frame(width: 100), content: {
ForEach(array, id: \.self, content: { word in
Text(word).tag(word)
})
})
.pickerStyle(MenuPickerStyle())
.padding()
.background(Color(.systemBackground).edgesIgnoringSafeArea(.all))
.cornerRadius(5)
}
}
struct ContentView: View {
var body: some View {
ZStack {
Color.gray
MenuPicker()
}
}
}

It looks like it's a bug with:
public init(selection: Binding<SelectionValue>, label: Label, #ViewBuilder content: () -> Content)
You can try replacing it with:
public init(_ titleKey: LocalizedStringKey, selection: Binding<SelectionValue>, #ViewBuilder content: () -> Content)
Here is a workaround (you can only use String as the label):
Picker(selection, selection: $selection) {
ForEach(array, id: \.self) { word in
Text(word).tag(word)
}
}
.frame(width: 100)
.animation(nil)
.pickerStyle(MenuPickerStyle())

Related

SwiftUI TabView is not working properly on Orientation Change

I created a simple tabView like this
struct ContentView: View {
var body: some View {
TabView{
TabItemView(color: .red, title: "Page 1")
TabItemView(color: .yellow, title: "Page 2")
TabItemView(color: .green, title: "Page 3")
TabItemView(color: .blue, title: "Page 4")
}
.tabViewStyle(.page)
.background(Color.indigo)
}
}
where
struct TabItemView: View {
var color : Color
var title : String
var body: some View {
Text("\(title)")
.frame(width:UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(color)
}
}
issue is when is switch to landscape entire thing is broken , like this
ie the tabs are jumped back to either some random position or position between two.
I searched online and some resources say its a bug thats not solved yet . Any solution for this?
Hi the bug is well known. Generally it helps when you put the tabview into inactive scrollview with certain frame modifier, like this:
struct ContentView: View {
var body: some View {
ScrollView([])
TabView{
TabItemView(color: .red, title: "Page 1")
TabItemView(color: .yellow, title: "Page 2")
TabItemView(color: .green, title: "Page 3")
TabItemView(color: .blue, title: "Page 4")
}
.tabViewStyle(.page)
.background(Color.indigo)
}
}
.frame(width: UIScreen.main.bounds.width)
}
This should fix the views in the middle in case of rotations. However, it might happen that when you use some array and iterate over it using ForEach inside the TabView, the elements are arbitrarily changed by TabView while rotating view. In that case it helps to keep tracking the current and previous orientation using states and build some logic onChange to prevent TabView to doing that. Like some wrapper adding layer between the binded state. Like:
struct TabView: View {
#State var pages: Int = 1
var array: [Int] = [1,2,3,4,5]
var body: some View {
VStack {
TabViewContent(selection: $pages, usage: .one) {
ForEach(array, id: \.self) { index in
Text("This is: \(pages) \(index)")
.tag(index)
}
}
}
}
}
Wrapper with logic:
struct TabViewContent<Selection: Hashable, Content: View>: View {
#Binding var selection: Selection
#State var selectionInternal: Selection
#State var previousOrientation: UIDeviceOrientation = .unknown
#State var currentOrientation: UIDeviceOrientation = .unknown
#ViewBuilder var content: () -> Content
internal init(selection: Binding<Selection>, content: #escaping () -> Content) {
self.content = content
_selection = selection
_selectionInternal = State(initialValue: selection.wrappedValue)
}
var body: some View {
TabView(selection: $selection, content: content)
.tabViewStyle(.page)
.onChange(of: UIDevice.current.orientation) { newOrientation in
currentOrientation = newOrientation
}
.onChange(of: selectionInternal) { value in
if currentOrientation == previousOrientation {
selection = selectionInternal
}
previousOrientation = currentOrientation
}
}
}

SwiftUI Navigation backward to certain View

I have a NavigationView which have multiple layers of NavigationLink.
e.g. A1->A2->A3->A4->A5.
struct A1: View {
var body: some View {
VStack{
Text("this is A1")
NavigationLink("to a2", destination: A2())
}
}
}
struct A2: View {
var body: some View {
VStack{
Text("this is A2")
NavigationLink("to a3", destination: A3())
}
}
}
struct A3: View {
var body: some View {
VStack{
Text("this is A3")
NavigationLink("to a4", destination: A4())
}
}
}
struct A4: View {
var body: some View {
VStack{
Text("this is A4")
NavigationLink("to a5", destination: A5())
}
}
}
struct A5: View {
var body: some View {
VStack{
Text("this is A5")
NavigationLink("to A2", destination: A2())
}
}
}
However, this only stack another A2() up to 6th level instead navigate back to the second level.
I've been noticed there is a #Environment(\.presentationMode) var mode: Binding<PresentationMode> and self.mode.wrappedValue.dismiss() to do the programally navigate back, but still can't find a way to do it multiple time at once.
Please help me.
You need to use the following NavigationLink in A2.
init(destination: Destination, isActive: Binding<Bool>, label: () -> Label)
Creates a navigation link that presents the destination view when active.
https://developer.apple.com/documentation/swiftui/navigationlink/init(destination:isactive:label:)
Use the following NavigationLink in A3 and A4.
init(destination: () -> Destination, label: () -> Label)
Creates a navigation link that presents the destination view.
https://developer.apple.com/documentation/swiftui/navigationlink/init(destination:label:)-27n7s
In A5, use a Button instad of NavigationLink.
And pass isActive upto A5. Toggle isActive when the button is clicked.
In this example, A2 is the view that you want to return to.
struct A2: View {
#State private var isActive: Bool = false
var body: some View {
NavigationView {
NavigationLink(destination: A3(isActive: self.$isActive), isActive: $isActive) {TText("this is A2") }
}
}
}
struct A3: View {
#Binding var isActive: Bool
var body: some View {
VStack {
NavigationLink(destination: A4(isActive: self.$isActive)) {Text("this is A3"))}
}
}
}
struct A4: View {
#Binding var isActive: Bool
var body: some View {
VStack {
NavigationLink(destination: A5(isActive: self.$isActive)) {Text("this is A4"))}
}
}
}
struct A5: View {
#Binding var isActive: Bool
var body: some View {
Button(action: {
self.isActive.toggle()
}, label: {
Text("this is A5")
})
}
}

ScrollView and .sheet

I have a ScrollView with a ForEach loop, each rendering a View. In the View I have 3 renders of the below ActionItem (a button that displays a sheet). The sheet does not show up with ScrollView but does with List. I'd normally attach the .sheet at the ScrollView layer however, with each button rendering a different view it seems more appropriate to nest it.
How I could get this to work with ScrollView? I'm using Xcode 12
struct ActionItem<Content>: View where Content : View {
public var text: String
public var icon: String
public var content: Content
#State var isPresented = false
init(text: String, icon: String, #ViewBuilder content: () -> Content) {
self.text = text
self.icon = icon
self.content = content()
}
var body: some View {
Button (action: {
DispatchQueue.main.async {
withAnimation {
self.isPresented = true
}
}
}) {
HStack(spacing: 2) {
Image(systemName: icon).font(.system(size: 14, weight: .semibold))
Text(text).fontWeight(.semibold)
}.padding([.top, .bottom], Dimensions.spacing)
.padding([.leading, .trailing], Dimensions.spacingMedium)
}.foregroundColor(Color.gray).font(.subheadline).background(Color.grayWhiteTer)
.cornerRadius(Dimensions.spacing)
.sheet(isPresented: $isPresented) {
self.content
}
}
}
In the View I'd render ActionItem such as Text, this also occurs if the View is ignored and the ActionItem is just directly in the ForEach. Same issue, sheet does not appear.
ActionItem(text: "", icon: "pencil") {
Text("ok")
}
The list looks like this
import SwiftUI
struct ItemsList: View {
#ObservedObject var itemModel: ItemModel
var body: some View {
VStack(alignment: .center, spacing: 0) {
ScrollView {
VStack {
ForEach(itemModel.items, id: \.self) { item in
ItemView(item: item)
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}}
Suggested callback update
struct ActionItem<Content>: View where Content : View {
public var text: String
public var icon: String
public var content: () -> Content
#State var isPresented = false
init(text: String, icon: String, #ViewBuilder content: #escaping () -> Content) {
self.text = text
self.icon = icon
self.content = content
}
var body: some View {
Button (action: {
DispatchQueue.main.async {
withAnimation {
self.isPresented = true
}
}
}) {
HStack(spacing: 2) {
Image(systemName: icon).font(.system(size: 14, weight: .semibold))
Text(text).fontWeight(.semibold)
}.padding([.top, .bottom], Dimensions.spacing)
.padding([.leading, .trailing], Dimensions.spacingMedium)
}.foregroundColor(Color.gray).font(.subheadline).background(Color.grayWhiteTer)
.cornerRadius(Dimensions.spacing)
.sheet(isPresented: $isPresented) {
self.content()
}
}
}
Try saving content as a callback (i.e. () -> Content) and call it in the sheet method instead of calling it in the initializer.. This will change when the view is created.

SwiftUI Programmatically Select List Item

I have a SwiftUI app with a basic List/Detail structure. A new item is created from
a modal sheet. When I create a new item and save it I want THAT list item to be
selected. As it is, if no item is selected before an add, no item is selected after
an add. If an item is selected before an add, that same item is selected after the
add.
I'll include code for the ContentView, but this is really the simplest example of
List/Detail.
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addNewResort = false
#State private var coverDeletedDetail = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
List {
ForEach(resortStore.resorts) { resort in
NavigationLink(destination: ResortView(resort: resort)) {
HStack(spacing: 20) {
Image("FlatheadLake1")
//bunch of modifiers
VStack(alignment: .leading, spacing: 10) {
//the cell contents
}
}
}
}
.onDelete { indexSet in
self.removeItems(at: [indexSet.first!])
self.coverDeletedDetail.toggle()
}
if UIDevice.current.userInterfaceIdiom == .pad {
NavigationLink(destination: WelcomeView(), isActive: self.$coverDeletedDetail) {
Text("")
}
}
}//list
.onAppear(perform: self.selectARow)
.navigationBarTitle("Resorts")
.navigationBarItems(leading:
//buttons
}//body
func removeItems(at offsets: IndexSet) {
resortStore.resorts.remove(atOffsets: offsets)
}
func selectARow() {
//nothing that I have tried works here
print("selectARow")
}
}//struct
And again - the add item modal is extremely basic:
struct AddNewResort: View {
//bunch of properties
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName, country: self.resortCountry, description: self.resortDescription, imageCredit: "Credit", price: Int(self.resortPriceString) ?? 0, size: Int(self.resortSizeString) ?? 0, snowDepth: 20, elevation: 3000, runs: 40, facilities: ["bar", "garage"])
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
To show the issue - The list with a selection:
The list after a new item created showing the previous selection:
Any guidance would be appreciated. Xcode 11.4
I tried to reconstitute your code as closely as could so that it builds. Here is what I have in the end. We have a list of resorts and when a new resort is saved in the AddNewResort sheet, if we are currently in split view (horizontalSizeClass is regular), we will select the new resort, otherwise just dismiss the sheet.
import SwiftUI
class ResortStore: ObservableObject {
#Published var resorts = [Resort(id: UUID(), name: "Resort 1")]
}
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addingNewResort = false
#State var selectedResortId: UUID? = nil
var navigationLink: NavigationLink<EmptyView, ResortView>? {
guard let selectedResortId = selectedResortId,
let selectedResort = resortStore.resorts.first(where: {$0.id == selectedResortId}) else {
return nil
}
return NavigationLink(
destination: ResortView(resort: selectedResort),
tag: selectedResortId,
selection: $selectedResortId
) {
EmptyView()
}
}
var body: some View {
NavigationView {
ZStack {
navigationLink
List {
ForEach(resortStore.resorts, id: \.self.id) { resort in
Button(action: {
self.selectedResortId = resort.id
}) {
Text(resort.name)
}
.listRowBackground(self.selectedResortId == resort.id ? Color.gray : Color(UIColor.systemBackground))
}
}
}
.navigationBarTitle("Resorts")
.navigationBarItems(trailing: Button("Add Resort") {
self.addingNewResort = true
})
.sheet(isPresented: $addingNewResort) {
AddNewResort(selectedResortId: self.$selectedResortId)
.environmentObject(self.resortStore)
}
WelcomeView()
}
}
}
struct ResortView: View {
let resort: Resort
var body: some View {
Text("Resort View for resort name: \(resort.name).")
}
}
struct AddNewResort: View {
//bunch of properties
#Binding var selectedResortId: UUID?
#State var resortName = ""
#Environment(\.presentationMode) var presentationMode
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#EnvironmentObject var resortStore: ResortStore
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName)
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
if self.horizontalSizeClass == .regular {
self.selectedResortId = newResort.id
}
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
struct WelcomeView: View {
var body: some View {
Text("Welcome View")
}
}
struct Resort {
var id: UUID
var name: String
}
We need to keep track of the selectedResortId
We create an invisible NavigationLink that will programmatically navigate to the selected resort
We make our list row a Button, so that the user can select a resort by tapping on the row
I started writing a series of articles about navigation in SwiftUI List view, there are a lot of points to consider while implementing programmatic navigation.
Here is the one that describes this solution that I'm suggesting: SwiftUI Navigation in List View: Programmatic Navigation. This solution works at the moment on iOS 13.4.1. SwiftUI is changing rapidly, so we have to keep on checking.
And here is my previous article that explains why a more simple solution of adding a NavigationLink to each List row has some problems at the moment SwiftUI Navigation in List View: Exploring Available Options
Let me know if you have questions, I'd be happy to help where I can.

Customized segmented control in Swift?

I would like to recreate the same effect than Pinterest:
Given that I'm new in Swift, I have three simple questions:
Is the menu a segmented control customized? or something like buttons?
How can I create this effect/animation of sliding? Is this a collectionView or something like that?
And finally, is it possible to create this with storyboard or the viewController needs to be created in full code ?
my two cents for highly customisable segmented, with colors and fonts.
struct ContentView: View {
#State private var segmentedSelection = 0
let titles = ["AA", "BB", "CC"]
let colors = [Color.red, .green, .white]
var body: some View {
VStack {
CustomSegmentedControl(segmentedSelection: $segmentedSelection, titles: titles, colors: colors)
Spacer()
Text("Hello, selection is " + titles[segmentedSelection] )
.padding()
}
}
}
struct CustomSegmentedControl: View {
#Binding var segmentedSelection : Int
var titles : [String]
let colors : [Color]
var body: some View {
VStack {
HStack{
ForEach(0 ..< titles.count, id: \.self){ (i: Int) in
Button(action: {
print(titles[i])
segmentedSelection = i
}, label: {
Text(titles[i])
.foregroundColor(colors[i])
.font(.system(size: i == segmentedSelection ? 22 : 16))
})
.frame(minWidth: 0, maxWidth: .infinity)
}
}
}
}
}

Resources