Controlling the look of Labels in SwiftUI Pickers - swiftui

I'm curious, is there a way to control how Label views are expressed in SwiftUI Pickers? Of course, one of the features of SwiftUI is that controls express themselves in different ways in different contexts (eg form vs not-form, iOS vs macOS, in toolbar vs out of toolbar).
Normally this feature is desirable, but in my situation, I find the disconnect of how the same label looks in the following three contexts to be a bit jarring:
When displaying the current selection of a Picker.
When displaying the choices available in the Picker menu.
When simply displaying information in a form.
In particular, 1 vs 2 is jarring. It doesn't seem right that the user chooses something from picker menu, where the SFSymbol is on the right, and the selection is shown with the SFSymbol on the left. And the way the label looks in context 1, the picker selection, is unattractive to me. The symbol and text are uncomfortably close.
Is there a way to override the default expressions so these differences are less jarring in this context? I actually would like it if all Labels could be expressed as something close to situation 3 (but with a narrower gap between SFSymbol and Text).
Here is some code to demonstrate this problem:
struct LabelInPickerLayout: View {
#State private var selection = 2
var body: some View {
Form {
Picker("Choice", selection: $selection) {
Label("First Item", systemImage: "1.circle").tag(1)
Label("Second Item", systemImage: "2.circle").tag(2)
}
.pickerStyle(.menu)
.labelsHidden()
Section("almost desired look") {
Label("First Item", systemImage: "1.circle")
}
}
}
}

Related

Sheet can't be dismissed when removing a tab

In my app, I have two tabs. The second tab is shown or hidden based on some condition. I find if there is a sheet being presented in the second tab when the tab is to be hidden, the sheet can't be dismissed.
The issue can be consistently reproduced with the code below. To reproduce it, click tab 2, then click "Present Sheet", then click "Hide Tab 2". You will see the sheet isn't removed, though the tab containing it (that is, tab 2) has been removed (you can drag the sheet down to verify it).
It seems a SwiftUI bug to me. Does anyone know how to work around it? I'm close to finish my app but hit this unexpected issue :( Any help will be much appreciated.
struct ContentView: View {
#State var showTab2: Bool = true
var body: some View {
TabView {
// tab 1
NavigationView {
Text("Tab 1")
}
.tabItem {
Label("Tab 1", systemImage: "1.circle")
}
// tab 2
if showTab2 {
NavigationView {
Tab2(showTab2: $showTab2)
}
.tabItem {
Label("Tab 2", systemImage: "2.circle")
}
}
}
}
}
struct Tab2: View {
#State var showSheet: Bool = false
#Binding var showTab2: Bool
var body: some View {
VStack(spacing: 12) {
Text("Tab 2")
Button("Click to present sheet") {
showSheet = true
}
}
.sheet(isPresented: $showSheet, onDismiss: nil) {
NavigationView {
MySheet(showTab2: $showTab2)
}
}
}
}
struct MySheet: View {
#Environment(\.dismiss) var dismiss
#Binding var showTab2: Bool
var body: some View {
Button("Click to hide tab 2") {
// dismiss() works fine if I comment out this line.
showTab2 = false
dismiss()
}
}
}
I have submitted feedback on this to Apple, but I'm not optimistic for any reply (I have never received one).
Update:
The issue can be reproduced in many other scenarios where no sheet is involved. So, the second approach #Asperi gave is not a general solution.
Well, here we see conflict of actions (due to racing): async sheet closing (due to animation) and sync tab removing.
Here are possible approaches:
delay tab removing after sheet closed (implicit way)
Button("Click to hide tab 2") {
dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { // << here !!
showTab2 = false
}
}
remove tab after sheet closed (explicit way)
.sheet(isPresented: $showSheet, onDismiss: { showTab2 = false }) { // << here !!
NavigationView {
MySheet(showTab2: $showTab2)
}
}
Note: Actually when view knows/manages something for parent of parent is not very good design, so option 2 (maybe with some additional conditions/callbacks) are more preferable.
#Asperi gave a great answer. But it's not straightforward to apply his approaches in actual app. I'll explain why and how to do it below.
The key idea in Asperi's approaches is that, since the UI changes have race condition, they should be performed in two steps. In both approaches the sheet is dismissed first, then the tab is hidden.
In practice, however, it may not be obvious how to decouple the two steps. For example, my app works this way (I think it's typical):
The sheet contains a form and call data model API to mutate data model when the form is submitted by user.
Since the data model API may fail, the sheet doesn't dismiss itself as soon as user submits the form. Instead it does that only when the API call succeeds (the API call is synchronous).
When the data model is mutated, it may trigger the condition to hide the tab.
Note the item 2 and 3. It means the sheet have to call data model API first, which may hide the tab, and then dismiss itself.
It took me a while to think out the solution - introduce a dedicated state to control show/hide the tab and hence decouple the two steps. Now the issue left is how to synchronize data model change to that state. Since the purpose is to make them to appear as two separate changes to UI, we can't use Combine. It can be messy if not implemented property because data model can be mutated from everywhere (e.g. Form, ActionSheet, or just Button). Fortunately I find a very elegant approach:
.onChange(of: model.showTab2) { value in
// In my experiments async() works fine, but just to be on the safe side...
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// This is a state outside data model. It hides/shows tab2.
showTab2 = value
}
}
This is another example that there is no problem that can't be solved by adding another layer of abstraction :)

Implementing Swift UI Picker

What is the correct way to implement a Picker component with specific logic within a Section element?
I would like to have each type displayed in a separate row.
var types = ["Books", "Films", "Music"]
var body: some View {
Form {
Section(header: Text("Type")) {
TextField("Type", text: $newCategoryType)
// Picker
}
}
}
First you must have a #State property that can be updated based on what selection the user makes, say in this case we have this
#State private var selectedType = "Books"
Then you will implement a Picker SwiftUI struct as follows
Picker("Please choose a type", selection: $selectedType) {
ForEach(types, id: \.self) {
Text($0)
}
}
Note that the \.self is really important for ForEach to distinguish between each element inside the list, without which the Picker won't perform the selection action correctly.
The above is enough for doing the job of displaying each option as a row since that is the default behaviour of ForEach
Additionally if you want to customise the look and feel of the picker
you would like to see .pickerStyle() view modifier, for which the docs and examples are mentioned
Also
Tip: Because pickers in forms have this navigation behavior, it’s important you present them in a NavigationView on iOS otherwise you’ll find that tapping them doesn’t work. This might be one you create directly around the form, or you could present the form from another view that itself was wrapped in a NavigationView.

How to get rid of SwifTUI List weird animation and spacing triggered by EditMode?

If you create a new project on Xcode 12.5 and use a SwiftUI List combined to a foreach and .onmove, you will notice 2 annoying things:
1/ Even though ondelete is unset, and even though you explicitly specify .deleteDisabled(true), a space appears on the left side of the list row when in EditMode. Is it possible to get rid of it?
2/ The entrance of the move icon creates some kind of an animation glitch on each list row. The divider is highlighting that. Why is there a space after the divider when EditMode is triggered? It seems that back in the past the divider width stay unchanged during this transition.
If you ask me, the previous behavior was making me think that the move button was embedded in a ZStack wrapping both the move button and the list. Whereas now it looks like it's in an HStack. Whatever, none of that explains the glitch which doesn't appear on the left side of the row even though it gets pushed too.
I am so confused.
I am surprised I am not able to find anything about that issue via Google. I am the one doing something wrong?
A video of the problem: Glitch and here is a simple code to try it out yourself.
Thank you for your time.
struct ContentView: View {
#State var listItems = ["Item 1", "Item 2", "Item 3"]
var body: some View {
NavigationView {
List {
ForEach(listItems, id: \.self) { (item) in
Text(item)
}.onMove { (indexSet, index) in
self.listItems.move(fromOffsets: indexSet,
toOffset: index)
}
}
.navigationBarTitle(Text("Nav Title"))
.deleteDisabled(true)
.navigationBarItems(trailing: EditButton())
}
}
}

How to do Apple Music-like navigation in SwiftUI? Custom List and NavigationView has highlight not going away

This is my example that I am trying to get to work:
struct ContentView: View {
let links = ["Item 1", "Item 2", "Item 3", "Item 4"]
var body: some View {
NavigationView {
ScrollView {
Text("My Title")
List(links, id: \.self) {
link in
NavigationLink(destination: TestView()) {
Text(link)
.padding(.vertical, 4)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.frame(height: 178)
Text("Some more content here")
}
}
}
}
Note: TestView is just some view with the text hello world on it.
I am trying to copy Apple Music's style of navigation. I tried putting a Button in the NavigationLink but tapping it on the text wouldn't change views, and I couldn't find a way to reliably change the color of the row when tapped, at the same time. Also in some approach, I managed to make it work, but the way the colors animate is different, i.e. it fades from A to B, over ~100ms whereas what I'm trying to achieve is to animate between the states instantly (like in Apple Music).
My current approach is using a List, putting NavigationLinks inside it and then cutting off the whole view by giving it a height. This way I can put it alongside other content.
It's working fine for now, but whenever I click on an row and go back, the row is still highlighted, when it shouldn't. Is there a way to make it so that it deselects when going back to the screen somehow?
I think this bug is being caused by the List being inside a ScrollView, since when I removed ScrollView, the list worked properly, and there wasn't this highlight bug. But I need to be able to put my content with the list, and I don't intend to have a list take up the whole screen.
Is there any way to fix this bug with this approach? I'm also willing for other ways to achieve the same result without using List.
Trying to use ForEach instead ofList?
With a view for row (CustomRow) where you can pass link item and set custom dividing line, background etc ...
ForEach(links, id: \.self) { link in
NavigationLink(destination: TestView()) {
CustomRow(item: link)
}
}
.frame(height: 178)

Rounded corners in swiftui list sections

Here is a screen shot of iOS 13 Health app - User profile. I recently started with swiftui and wondering how to develop a screen like below. I tried list styles plain and grouped. But I couldn't get the look of below layout.
Can UI like this develop purely using swiftui-list?
I am specially looking for rounded sections and including a image inside the list.
As of iOS 14, you can use the code below. It works just perfectly, just like in UIKit.
List {
Section {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
Section {
Text("Item 4")
Text("Item 5")
Text("Item 6")
}
}.listStyle(InsetGroupedListStyle()) // this has been renamed in iOS 14.*, as mentioned by #Elijah Yap
.environment(\.horizontalSizeClass, .regular)
Thank you.
As of iOS 14.0, this list-style is .listStyle(InsetGroupedListStyle()) or .listStyle(.insetGrouped)
I run into the same problem and it looks like you can not achive this with list or section.
When you put cornerRadius modifier onto a section or list it is just applied to whatever the internals are making cells rounded while section itself being unchanged.
My solution was to avoid lists and sections entirely and build custom ones.
Something like
SectionView: View {
body: some View = {
VStack {
ForEach {
CellView()
}
}
.background(Color.white)
.cornerRadius(10)
.padding()
}
}
And put those sections over a Color view in some ZStack to have a gray background.
Take in account that the huge downside is that a ForEach will render all the cells right away, like if you have a thousand of cells then onAppear() will be called for each cell and strangely enough they will be called in backward order.
Hope it helps.
According to my findings and as of Xcode 11 GM seed 2 (11A420a), making this UI is not possible just from swiftui list.
There is a new list style available for storyboards named, insetGrouped. It has the exact same look.
https://developer.apple.com/documentation/uikit/uitableview/style