What is the generic type of HStack? - ios

I'm building a calculator app in SwiftUI as part of an iOS development course. The UI is as follows:
As we can see, there five rows of buttons, each button has a text, as well as the result panel is a text. Thus, extracting the text, buttons and rows into separate views works perfectly. Following is the code that creates a row.
struct RowView: View {
let buttons: [CalculatorButton]
#Binding var result: String
let calc: Calculator
var body: some View {
createCells(self.buttons)
}
private func createCells(_ buttons: [CalculatorButton]) -> some View {
if buttons.count % 2 == 0 {
return AnyView(HStack(spacing: 1) {
ForEach(buttons, id: \.label) { button in
ButtonView(button: button, result: self.$result, calc: self.calc)
}
})
} else {
return AnyView(HStack(spacing: 1) {
ButtonView(button: buttons[0], result: self.$result, calc: self.calc)
createCells(buttons.suffix(2))
})
}
}
}
The idea is that if there are even number of buttons in a row, they are simply stacked horizontally. For the last row, we manually place the first button, and then make a recursive call that places the remaining two buttons horizontally. Sweet!
However, the AnyView(HStack(spacing: 1) code is repeated in the if-else block. I tried to create a local variable for the contents of the HStack, but couldn't find a type that successfully compiled for both if and else.
Is it possible to do so? I'm using Xcode 11.5, with Swift 5.

Related

Swift table only show 1 column

I am new to Xcode, and facing some issues while creating a table view.
Problem: In iOS 14 Plus simulator, only 1 columns is shown - "Category" and without header. The entire column "Total" is completely missing. However, when I switch the simulation to ipad, everything is working fine. Really appreciate if someone could help to resolve this issue.
struct OverallObj: Identifiable {
var category: String
var total: Int
var id: String {
category
}
static var summary: [OverallObj] {
[
OverallObj(category: "1st", total: 1000),
OverallObj(category: "2nd", total: 1000)
]
}
}
var body: some View {
Table(overallView) {
TableColumn("Category", value: \.category)
TableColumn("Total") { overallObj in
Text("\(overallObj.total)")
}
}
}
I am expecting the table to show 2 columns - "Category" and "Total" with headers
The problem in your specific case is caused by the compact horizontal size on iOS.
From the official documentation:
MacOS and iPadOS support SwiftUI tables. On iOS, and in other
situations with a compact horizontal size class, tables don’t show
headers and collapse all columns after the first. If you present a
table on iOS, you can customize the table’s appearance by implementing
compact-specific logic in the first column.
You can implement compact-specific logic in the first column of a table in SwiftUI by using the environment modifier and checking the sizeCategory environment value.If the size category is compact, you can return a compact version of the view for the first column with all the data you need to display, otherwise return the normal version of the view.
Here is an example:
#Environment(\.horizontalSizeClass) var sizeCategory
var body: some View {
Table(overallView) {
TableColumn("Category") { item in
if (sizeCategory == .compact) {
// Return compact version of the view
Text("Compact")
} else {
// Return normal version of the view
Text("Normal")
}
}
TableColumn("Total") { overallObj in
Text("\(overallObj.total)")
}
}
Using tables on different platforms
macOS and iPadOS support SwiftUI tables. On iOS, and in other situations with a compact horizontal size class, tables don’t show headers and collapse all columns after the first. If you present a table on iOS, you can customize the table’s appearance by implementing compact-specific logic in the first column.
Source: https://developer.apple.com/documentation/SwiftUI/Table

SwiftUI NavigationSplitView column visibility on iPhone?

I'm trying to create a 3-column layout in SwiftUI.
The first column is a LazyVGrid of selectable items. The selection then impacts a list of items in a content view (second column) which also isn't a SwiftUI list but a VStack + other views in a scrollview. Selecting items on that column impacts the detail view.
I got it all to work on the iPad, but this is because the iPad displays multiple columns at a time and NavigationSplitView supports gestures on the iPad as well as column visibility settings in code.
The problem is that I can't find a way to programmatically navigate from one column to another on the iPhone as it doesn't seem to respond to column visibility bindings.
I initially had it working with navigation links for each item on my grid where the destination was set to a view, but the code smelled pretty bad.
Eventually, I came up with the code below. In the first column I have my grid view which has a custom onSelect modifier that I trigger whenever an item is selected. That's where I'm trying to change column visibility. I tried setting vm.navigationColumnVisibility = .detailOnly, but the iPhone seems to ignore it.
I was able to get it to work exactly as expected by changing the grid to a List. The selection property/Binding in the List view seemed to trigger navigation on the iPhone. However, that's not the desired UI/UX.
Any advice on how to trigger navigation between columns programatically on the iPhone or a better way to adapt this code to achieve the described UI/UX?
struct JNavSplitView: View {
#EnvironmentObject var vm: JNavSplitViewModel
#State private var book: Book? = nil
#State private var entry: Entry? = nil
var sheet: some View {
#if os(macOS)
JEntryCreateFormMacOS(book: book)
#else
JEntryCreateForm(book: book)
.onCreate { entry in
self.entry = entry
}
#endif
}
var body: some View {
NavigationSplitView(columnVisibility: $vm.navigationColumnVisibility) {
BooksGridView(selected: $book)
.onSelect { book in
self.entry = book.getEntries(forDate: Date()).first
}
} content: {
BookEntriesView(book: book, selected: $entry)
} detail: {
PageCollectionView(entry: $entry)
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
#if os(macOS)
.toolbarBackground(Color("Purple 1000"), for: .windowToolbar)
#endif
.environmentObject(vm)
.sheet(isPresented: $vm.presentNewEntryForm, content: {
sheet
})
}
}

How to use Transferable for Table Rows in SwiftUI

With WWDC 2022, Apple introduced the Transferable protocol to support Drag & Drop operations in an easy way. How can I use this new technique (in combination with the new draggable and dropDestination modifiers) for SwiftUI tables (not lists)?
The TableRowContent does not support the draggable and dropDestination modifiers. Also, when applying the modifiers directly to the views in the TableColumns, the drag / drop operation will only work on for that specific cell, and not the entire row. When adding the modifiers to all cells, it still does not work when dragging e.g. in an empty space inside of a row.
struct Item: Identifiable, Codable, Transferable {
let id = UUID()
let text: String
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .text)
}
}
struct Test: View {
var body: some View {
Table {
TableColumn("Column 1") { item in
Text(item.text)
.draggable(item) // This does only work when dragging not in the space between two columns
}
TableColumn("Column 2") { item in
Text(item.text)
.draggable(item) // This does only work when dragging not in the space between two columns
}
} rows: {
ForEach([Item(text: "Hello"), Item(text: "World")]) { item in
TableRow(item)
.draggable(item) // This causes a compile-time error
.itemProvider { ... } // This does not work with Transferable and thus support my use case
}
}
}
}
I want a similar behavior as the itemProvider modifier and the recently added contextMenu modifier on the TableRowContent , which allow the respective operation on the whole table row. I cannot use itemProvider, since it requires to return an NSItemProvider, which does not support my use case of dragging a file from a network drive to the Mac hard drive.
I found out that you can register a Transferable object in the NSItemProvider:
TableRow(item)
.itemProvider {
let provider = NSItemProvider()
provider.register(item)
return provider
}
Unfortunately the itemProvider closure is already called when the dragging session starts instead of when the drop was performed. But I guess this problem is a different question to answer.

SwiftUI: Incorrect UI when switching to a new list from an old one with swipe button shown

The issue can be reproduced consistently with the code below. Xcode 13.3 + iOS 15.4 (both are latest).
enum ListID: String, CaseIterable, Hashable, Identifiable {
case list1 = "List1"
case list2 = "List2"
var id: Self {
self
}
}
struct ContentView: View {
#State var listID: ListID = .list1
var body: some View {
VStack {
// 1) Picker
Picker(selection: $listID) {
ForEach(ListID.allCases) { id in
Text(id.rawValue)
}
} label: {
Text("Select a list")
}
.pickerStyle(.segmented)
// 2) List
switch listID {
case .list1:
createList(Array(1...2), id: .list1)
case .list2:
createList(Array(101...102), id: .list2)
}
}
}
#ViewBuilder func createList(_ itemValues: [Int], id: ListID) -> some View {
List {
ForEach(itemValues, id:\.self) { value in
Text("\(value)")
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button("Edit") {
// do nothing
}
.tint(.blue)
}
}
}
.id(id)
}
}
Steps to reproduce the issue:
start up the app. See picture 1.
swipe item 1 in list 1, keep the "Edit" button untouched (don't click on it). See picture 2
then select list 2 in the picker. You should see there is an extra space before the items in the list. What's more, all list items are not swipable any more. See picture 3.
then select list 1 in the picker. It has the same issue. See picture 4.
The issue is not specific to picker. For example, it can be reproduced if we replace picker with a set of buttons. I believe the issue occurs only if the old list is destroyed in SwiftUI view hierarchy. From my understanding of structured identity in SwiftUI, list 1 and list 2 are considered separate views in SwiftUI view hiearch. So it's not clear how they could affect each other. The only reason I can guess is that, while list 1 and list 2 are considered separate virtual views, SwiftUI actually use the same physical view for them (e.g., for performance purpose, etc.). So it seems a SwiftUI bug to me.
Thinking along that line, I can work round the issue by not destroying lists:
ZStack {
createList(Array(1...2), id: .list1)
.opacity(listID == .list1 ? 1 : 0)
createList(Array(101...102), id: .list2)
.opacity(listID == .list2 ? 1 : 0)
}
This works perfectly in this specific example, but unfortunately it's not scalable. For example, in my calendar app when user clicks on a date in the calendar, I'd like to show a list of events on the date (I'd like to use different list for different date. I do so by calling id() to set different id to each list). There isn't an obvious/elegant way to apply the above work around in this case.
So I wonder if anyone knows how to work around the issue in a more general way? Thanks.
I end up working around the issue by using a single virtual view for different lists. To that end, I need to move List outside switch statement (otherwise SwiftUI's structured identity mechanism would consider the two lists as different ones).
The workaround works reliably in my testing (including testing in my actual app). It's clean and general. I prefer to assigning a different id to each list because I think it's clean and better in architecture, but unfortunately it's not usable until Apple fixes the bug. I have submitted FB9976079 about this issue.
I'll keep my question open and welcome anyone leave your answer or comments.
enum ListID: String, CaseIterable, Hashable, Identifiable {
case list1 = "List1"
case list2 = "List2"
var id: Self {
self
}
}
struct ContentView: View {
#State var listID: ListID = .list1
var body: some View {
VStack {
// 1) Picker
Picker(selection: $listID) {
ForEach(ListID.allCases) { id in
Text(id.rawValue)
}
} label: {
Text("Select a list")
}
.pickerStyle(.segmented)
// 2) List
List {
switch listID {
case .list1:
createSection(Array(1...2), id: .list1)
case .list2:
createSection(Array(101...105), id: .list2)
}
}
}
}
// Note: the id param is not used as List id.
#ViewBuilder func createSection(_ itemValues: [Int], id: ListID) -> some View {
Section {
ForEach(itemValues, id:\.self) { value in
Text("\(value)")
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button("Edit") {
// do nothing
}
.tint(.blue)
}
}
}
.id(id)
}
}

Button handling in SwiftUI

In a SwiftUI app I have a few buttons (let us say 3 as an example). One of them is highlighted.
When I tap on a non-highlighted button, the previously highlighted button toggles to non-highlighted and the tapped button becomes highlighted. If I tap the already highlighted button, nothing happens.
This scenario is simple and I have a highlighBtn variable, when a button is tapped highlighBtn takes the value of the tapped button. This variable is then used when the next tap happens to toggle off the previously highlighted button.
This cycle is OK, but the problem is when I do the first tap. For some reasons, things don't work.
This is how I handle the creation of the highlighBtn variable:
class ActiveButton: ObservableObject {
#Published var highlighBtn = Button(....)
}
#StateObject var box = ActiveButton()
Here is the relevant code, when the button is tapped:
#EnvironmentObject var box: ActiveButton
....
Button(action: {
// Toggle off the previously highlighted button.
box.highlighBtn.highLight = false
.... some useful processing ....
box.highlighBtn = self
})
One detail I should give: if I tap the highlighted button to start, then all works as it should.
I have tried various method to solve this apparently simple problem but failed.
The first method was to try to initialize the highlighBtn variable.
The second was to try to simulate a tap on the highlighted button.
I must be missing something simple.
Any tip would be welcome.
After further investigation .....
I have created a demo app to expose my problem.
Since I lack practice using GitHub, it took me some time, but it is now available here.
For that I created a SwiftUI app in Xcode.
In SceneDelegate.swift, the four lines of code right after this one have been customized for the needs of this app:
// Create the SwiftUI view that provides the window contents.
Beside that all I did resides inside the file ContentView.swift.
To save some time to anyone who is going to take a look; here is the way to get to the point (i.e. the issue I am facing).
Run the app (I did it on an iPhone 7). You will see seven buttons appear. One of them (at random) will be highlighted. Starting with the highlighted button, tap on a few buttons one after another as many times as you want. You will see how the app is supposed to work.
(After switching it off) Run the app a second time. This time you will also tap on a few buttons one after another as many times as you want; but start by tapping one of the non-highlighted button. You will see the problem appear at this point.
Here is a solution for the first part of your question: Three buttons where the last one tapped gets highlighted with a background:
import SwiftUI
struct ContentView: View {
enum HighlightTag {
case none, first, second, third
}
#State private var highlighted = HighlightTag.none
var body: some View {
VStack {
Button("First") {
highlighted = .first
}.background(
Color.accentColor.opacity(
highlighted == .first ? 0.2 : 0.0
)
)
Button("Second") {
highlighted = .second
}.background(
Color.accentColor.opacity(
highlighted == .second ? 0.2 : 0.0
)
)
Button("Third") {
highlighted = .third
}.background(
Color.accentColor.opacity(
highlighted == .third ? 0.2 : 0.0
)
)
}
}
}
Update:
After reviewing your sample code on GitHub, I tried to understand your code, I tried to make some simplifications and tried to find a working solution.
Here are some opinions:
The Attribute "#State" in front of "var butnsPool" is not needed and confusing.
The Attribute "#State" in front of "var initialHilight" is not needed and confusing.
Your ActiveButton stores a copy of the selected Button View because it is a struct which is probably the main reason for the strange behaviour.
The needInit in your ObservableObject smells bad at least. If you really need to initialize something, you may consider doing it with some .onAppear() modifier in you ContentView.
There is probably no need to use .environmentObject and #EnvironmentObject. You could consider using a parameter and #ObsservedObject
There is probably no need for the ActiveButton at all, if you only use it internally. You could consider using a #State with the selected utton name
Your BtnTxtView is fine, but consider replacing the conditional (func if) with some animatable properties, if you want to animate the transition.
Based on your code I created a much simpler and working solution.
I removed the ActiveButton class and also the BttnView struct.
And I replaced the ContentView with this:
struct ContentView: View {
var butnsPool: [String]
var initialHilight: Int
#State var selectedBox: String = ""
var body: some View {
ForEach(butnsPool, id: \.self) { buttonName in
Button(action: {
selectedBox = buttonName
})
{
BtnTxtView(theLabel: buttonName,
highLight: buttonName == selectedBox)
}
}.onAppear {
selectedBox = butnsPool[initialHilight]
}
}
}

Resources